Skip to content
Tech Stack20 July 2026 · 8 min readUpdated 30 September 2026

Inngest for SaaS Background Jobs in 2026: When and How

Inngest runs background jobs as durable steps with per-step retries, but its free plan counts every step as an execution. Here's when it beats BullMQ, and when a plain queue is enough.

Inngest for SaaS Background Jobs in 2026: When and How

If one of your SaaS jobs has more than one step that can fail on its own, count those steps before you pick a tool. That count decides whether a plain queue is enough or whether a durable workflow platform like Inngest earns its place. A single webhook handler that syncs one field needs no such platform. A flow that enriches a lead, waits for a human approval and then sends an email probably does.

BookBed sits at the simple end. Its case study lists Stripe webhook handlers and a trial lifecycle on a Flutter, Firebase and Stripe stack, and in my judgment none of that needs a durable-execution platform. This post covers where Inngest fits for background jobs, what its pricing page says today, and how to move one job over without a big-bang rewrite.

What are Inngest background jobs, and when does a SaaS need them?

One healthy green leaf among several on a stem glows through with cyan veins as it regrows from a wilted brown leaf, while the rest of the stem's leaves stay untouched

Inngest background jobs are functions that run in response to an event or a cron schedule, with every step.run() call retried on its own, so a failure partway through repeats only that step and not the whole function. The Inngest background jobs guide describes the model: you send an event with inngest.send(), Inngest finds the functions triggered by that event and runs them in the background, and you don't create queues, workers or subscriptions yourself.

Two details from the docs change how you write code. First, when step.run() finishes successfully, its response is saved in the function run state and the step doesn't run again. Second, by default Inngest retries a function or a step up to 4 times in addition to the initial attempt, with exponential backoff and jitter, and each step keeps its own retry counter.

The third piece is waiting. step.sleep() and step.sleepUntil() pause a run without consuming compute, and step.waitForEvent() pauses until a matching event arrives, with an optional timeout. The same steps page says functions can sleep for at most a year, and seven days on the free plan. That's enough for a trial-expiry reminder or a human approval, and it removes the polling table most queue-based systems eventually grow.

Why do queues guarantee delivery but not completion?

Two seed pods hang from one branch: the left pod has burst into an empty dry husk, the right pod has fully bloomed into an open cyan-centered flower

A queue guarantees a job reaches a worker, and it says nothing about whether the multi-step process behind that job finishes. Inngest's own engineering blog makes this argument and names BullMQ, Celery and Sidekiq as queues that guarantee delivery. It also offers a test: if you can describe the work as a single verb, such as send, process, sync or generate, it's probably job-shaped. That's the vendor's heuristic, so treat it as a starting point and not a verdict.

For atomic work, a plain queue is the right tool. Send a receipt email, write one row, ping a webhook, and if it fails, retry the whole thing cheaply. Adding step-level durability to a job with no intermediate state to lose is ceremony.

The distinction matters once a job has a middle. Consider a two-person team building lead outreach (a hypothetical, not a client). The flow enriches a lead, scores it, drafts an email, waits for a human to approve it, then sends it. On a raw queue that's five or six chained jobs, your own state table tracking which one is done, and a poller watching for the approval. As one Inngest function, each step.run() retries independently and the approval wait holds no worker. The same Inngest post warns about the catch: if step one charged a payment and step four failed, you may charge twice on resume. Durable retries only work when each step is safe to run twice.

When do you not need Inngest at all?

You don't need Inngest when the whole job is one short action with a clear retry story. Pizzeria Bestek is the example I'd point to. Per its case study, three Supabase Edge Functions run the admin flow, including one that sends the order emails and one that the CONFIRM and DECLINE buttons call, at five to ten orders a day. At that volume and job shape, a durable platform would solve a problem the business doesn't have.

The same logic applies if you already run Supabase Edge Functions for latency-sensitive endpoints. Keep them there. Inngest is for the durable, multi-step, can-wait work next to them, and a job triggered by a third-party webhook works the same way as one triggered by an internal event, only the trigger changes.

Inngest vs BullMQ: which one fits your stack?

A single root splits into two paths: one winds through a rigid grid of dark metal pipes, the other grows freely through open sunlit soil into small cyan flowers

Inngest fits serverless deployments with multi-step or long-waiting workflows, and BullMQ fits teams that already run Redis and want direct control over queue behaviour. Both solve real problems, and neither is better in the abstract. The comparison below sticks to what each vendor's own docs state.

InngestBullMQ
What it isEvent-driven platform, cloud-orchestrated or self-hosted as a single binaryA Node.js library built on Redis
InfrastructureNo queue or worker to run yourself on the cloud planRedis you run or rent, plus your own worker processes
RetriesPer step, default 4 retries plus the initial attemptRetries of failed jobs
Long waitsstep.sleep(), step.sleepUntil(), step.waitForEvent()Delayed jobs
Multi-step workSteps inside one functionFlows with parent-child dependencies
ExtrasCron triggers, per-step stateRate limiting, priorities
PricingFree plan at $0, then paid tiersLibrary plus your own Redis bill
Best fitServerless, multi-step or AI workflowsExisting Redis, high-throughput, priority control

Notice that BullMQ isn't limited to single-step work, since its docs list flows with parent-child dependencies. The real difference is where job state lives and who operates it. With BullMQ, you own Redis and the worker fleet. With Inngest cloud, the platform holds the run state, and you pay for that with metered executions. If you already have Redis and a team that's comfortable running it, BullMQ is a sound default. If you deploy to serverless and your jobs wait on people or external systems, Inngest removes a whole layer.

How much does Inngest cost, and what does self-hosting give you?

Inngest's pricing page lists a Free plan at $0 with 50k executions a month and 5 concurrent steps, Pro from $99 a month with 1M executions, and Business from $499 a month with 10M. Free also carries a 256 KiB event size limit and 24-hour trace history, per the same page (checked 30 September 2026).

Why does the execution count catch people out?

Because an execution is metered per function run and per step inside that run. The pricing page's own example: a function with 5 step.run() calls uses 6 executions, one for the run and five for the steps. Take a five-step function that runs 300 times a day. That's 1,800 executions a day, and you pass the 50,000 free monthly allowance around day 28. Sketch your real step count per function before you assume the free plan covers an MVP.

Two more free-plan details are worth knowing. The cron docs say a scheduled function that fails 20 times in a row is automatically paused on the free plan, and they recommend UTC over local time zones because schedules near daylight-saving changes can run zero, one or two times in a day.

Is self-hosting Inngest a real escape hatch?

It's a real option with clear limits you should read before you commit. The self-hosting docs describe a single binary that includes all Inngest services. By default it uses SQLite for persistence and an in-memory Redis server for the queue and state store, and it needs an event key and a signing key to start. The same page states that Inngest's support team doesn't guarantee direct support for self-hosted instances, and it offers a Helm chart described as production-ready Kubernetes deployments.

My reading, which is an inference: the in-memory default suits local development, and the Helm chart is the path for anything that must survive restarts. Check the persistence options in the docs before you put durable jobs on a default install. If you self-host expecting Inngest Cloud minus the bill, you'll be operating the platform yourself.

How do you migrate a BullMQ job to Inngest?

Migrate one job at a time: pick a job with real multi-step state, wrap each unit of work in its own step.run(), and run the old and new paths side by side before you cut over. This works on most Next.js or serverless backends, and it's a recommendation, not a guarantee.

  1. Pick a job with real intermediate state. A single-step email send proves nothing about whether the migration pays off.
  2. Replace the queue's add() call with an Inngest event send, keeping your existing payload shape so the rest of the app stays untouched.
  3. Wrap each unit that has its own try/catch and its own "mark this step done" flag in a separate step.run().
  4. Turn any manual delay, cron poll or "check again in an hour" loop into step.sleep() or step.sleepUntil(), and delete the polling table it replaces.
  5. Run the old queue path and the new function in parallel behind a flag for a full billing cycle, and compare completion rates.
  6. Retire the old worker only after the parallel run shows equal or better completion.

Don't migrate everything at once. You'd spend a week debugging which system owns a job's state.

Idempotency comes first for anything that touches money. Stripe's API supports idempotency keys so a repeated request returns the saved result of the first one instead of performing the operation twice, and all POST requests accept them. Pass a key derived from your step's identity, and a retried step can't double-charge.

The Callidus case study shows the current alternative. On React, Firebase and Stripe Connect, a coordinator Cloud Function runs daily at 09:00 UTC and fans out one Cloud Task per trialing tenant, and a second scheduled function at 09:30 UTC escalates failed payments. That works without Inngest. Here's a labelled hypothetical for where a durable workflow would change things in a B2B SaaS: a nightly per-tenant reconciliation where one tenant's Stripe call gets rate-limited. With per-step retries, only that tenant's step repeats. This isn't something Callidus runs, and you should weigh it against the tools you already have.

Still deciding what belongs where? Settle the rest of your SaaS MVP stack first, since auth, billing and data isolation shape this choice more than the job runner does.

Open your most fragile background job and count its steps. How many can fail independently, and does anything in your code mark which of them already finished?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What are Inngest background jobs, and how do they differ from a queue?

Inngest background jobs are functions triggered by an event or a cron schedule whose steps are saved and retried individually, whereas a queue only guarantees that a job reaches a worker. According to Inngest's steps docs, a step.run() that succeeds has its response stored in the run state and doesn't execute again. Inngest retries a failed step up to 4 times by default, with backoff. A queue like BullMQ can retry whole jobs, so the difference shows up when a job has several steps that can fail on their own or must wait for hours.

How much does Inngest cost, and is the free plan enough for a small SaaS?

The free plan is enough for a small SaaS only if your step counts are low, because it includes 50k executions a month and 5 concurrent steps. Inngest's pricing page says an execution is metered per function run and per step, so a function with 5 step.run() calls uses 6 executions. At 300 runs a day that's 1,800 executions daily, which passes 50,000 around day 28. Paid plans start with Pro at $99 a month and 1M executions. Count the steps in your busiest function before you assume the free plan covers it.

Can you self-host Inngest for production?

You can self-host Inngest, but read the limits before you put production jobs on a default install. The self-hosting docs describe a single binary that uses SQLite for persistence and an in-memory Redis server for the queue and state store by default, and that requires event and signing keys. They say Inngest's support team doesn't guarantee direct support for self-hosted instances, and they offer a Helm chart described as production-ready Kubernetes deployments. Check the persistence options in the docs, and plan to operate the platform yourself.

When should you use Inngest instead of BullMQ?

Use Inngest when your deployment is serverless and your jobs have several steps or long waits, and use BullMQ when you already run Redis and want direct queue control. BullMQ is a Node.js library built on Redis with delayed jobs, retries, rate limiting, priorities and flows. Inngest removes the queue and worker you'd otherwise run, at the cost of metered executions. If your jobs are single-step and you have Redis operations experience, BullMQ is a sound default, and a migration only pays off for jobs with real intermediate state.

How do you run scheduled jobs in Next.js with Inngest?

Define a function with a cron trigger, and Inngest runs it on schedule the same way it runs event-triggered functions. The cron docs show the timezone set inside the cron string, for example TZ=Europe/Paris 0 12 * * 5, and recommend UTC because schedules near daylight-saving changes can run zero, one or two times in a day. On the free plan, a scheduled function that fails 20 times in a row is paused automatically. That keeps scheduled and event-driven work in one system with one retry model.