The Best Workflow Providers for Next.js Apps in 2024
If you're building a Next.js app, sooner or later you hit the same wall: some piece of work doesn't belong in a request/response cycle. Sending emails, processing payments, running AI pipelines, syncing third-party APIs, generating reports — none of it should block a user, and none of it should die quietly when a serverless function times out.
That's where workflow providers come in. They give you durable execution, retries, scheduling, and observability on top of your existing Next.js routes. Here are the four worth considering in 2024, what they're good at, and where each one falls short.
What "workflow provider" actually means here
A workflow provider runs your background logic for you, durably. The two things that matter most:
- Durability — if a step fails or the runtime restarts, the workflow picks up where it left off instead of re-running the whole thing.
- Step-based execution — you break a job into discrete steps, each of which is retried and cached independently.
Plain Vercel Cron doesn't really fit this definition (no persistence, no retries, function timeout limits), but it's still worth knowing about for the simplest cases. The serious contenders are Inngest, Trigger.dev, and Upstash Workflow.
1. Inngest — event-driven workflows
Inngest takes an event-driven approach: you publish events, and one or more functions react to them. It plugs into Next.js through a single API route handler.
// lib/inngest.ts
import { Inngest, eventType, staticSchema } from 'inngest'
export const inngest = new Inngest({ id: 'my-app' })
const paymentCreated = eventType('payment.created', {
schema: staticSchema<{ paymentId: string }>(),
})
export const processPayment = inngest.createFunction(
{
id: 'process-payment',
triggers: [paymentCreated],
},
async ({ event, step }) => {
const payment = await step.run('validate', () =>
validatePayment(event.data.paymentId),
)
await step.run('email', () => sendConfirmation(payment.customerEmail))
await step.run('inventory', () => decrementInventory(payment.items))
},
)
// app/api/inngest/route.ts
import { serve } from 'inngest/next'
import { inngest } from '@/lib/inngest'
import { processPayment } from '@/lib/functions'
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [processPayment],
})
Each step.run is checkpointed. If the email step fails, only that step retries on the next execution — the validation step won't run twice.
Best for: complex fan-out workflows where one user action triggers many downstream functions, and you want event sourcing semantics out of the box.
Pricing: generous free tier (~1,000 runs/month), paid plans starting around $20/month. (comparison details)
2. Trigger.dev — developer experience first
Trigger.dev is the option people reach for when DX and observability matter most. The local dev tunnel, the dashboard, the per-task logs — it's the easiest of the bunch to debug when something goes wrong in production.
In v3, jobs are plain TypeScript functions running on Trigger's own infrastructure (not your Vercel functions), so cold starts and serverless timeouts stop being your problem.
// trigger/onboarding.ts
import { task } from '@trigger.dev/sdk/v3'
export const onboardUser = task({
id: 'onboard-user',
run: async (payload: { userId: string }) => {
const user = await createUserProfile(payload.userId)
await scheduleWelcomeEmails(user.email)
await initializeUserAnalytics(user.id)
return { userId: user.id }
},
})
You trigger it from a Next.js route or server action with tasks.trigger('onboard-user', { userId }).
Best for: workflows that integrate with lots of external APIs, long-running AI jobs, and teams that want a polished dashboard over rolling their own tooling.
Pricing: free tier around 1,000 runs/month, paid plans from $20/month with usage-based options for higher volume.
3. Upstash Workflow — durable on top of QStash
Upstash Workflow is the newest of the three and the most "Next.js native" in feel. It runs entirely inside your existing route handlers — there's no separate worker process, no second deployment target. Under the hood it's built on Upstash QStash, which calls back into your route between every step.
// app/api/workflow/route.ts
import { serve } from '@upstash/workflow/nextjs'
export const { POST } = serve<{ userId: string }>(async (context) => {
const user = await context.run('load-user', async () => {
return getUser(context.requestPayload.userId)
})
await context.run('send-email', async () => {
await sendWelcome(user.email)
})
await context.sleep('wait-a-day', '1d')
await context.run('follow-up', async () => {
await sendFollowUp(user.email)
})
})
Every context.run is a checkpoint. The route returns after each step, QStash schedules the next call, and your function picks up exactly where it left off — including across long sleeps, which is hard to do on raw serverless. The Upstash team uses it themselves at scale to coordinate webhooks and rate-limited external APIs.
Best for: Next.js apps already on Vercel that want durable execution without running a second service, and workflows with long sleeps or human-in-the-loop steps.
Pricing: pay-per-message via QStash, with a free tier on Upstash that covers small projects.
4. Vercel Cron — the simple case
Not a workflow engine, but worth mentioning because it's free with any Vercel project and covers a real need: scheduled tasks.
// vercel.json
{
"crons": [
{ "path": "/api/cron/daily-reports", "schedule": "0 9 * * *" },
{ "path": "/api/cron/cleanup", "schedule": "0 */6 * * *" }
]
}
// app/api/cron/daily-reports/route.ts
export async function GET(req: Request) {
if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`)
return new Response('Unauthorized', { status: 401 })
await generateAndSendReports()
return Response.json({ ok: true })
}
There's no retry logic, no persistence, and you're still bound by Vercel's function timeouts (10s on Hobby, up to 5 minutes on Pro). The moment a job needs to retry, fan out, or run longer than a few minutes, you've outgrown it.
Best for: simple recurring maintenance jobs — cleanup, cache warming, daily emails — where "good enough" really is good enough.
How to pick
A rough decision tree:
- Just need scheduled tasks under a few minutes? Vercel Cron.
- Lots of fan-out from a single event, multiple consumers per event? Inngest.
- Want the best dashboard and local dev experience, especially for AI/long-running jobs? Trigger.dev.
- Already on Vercel + Upstash and want durable workflows without a second service? Upstash Workflow.
There's no single winner here. The honest answer is that Inngest, Trigger.dev, and Upstash Workflow are all good — they just optimize for different things. Pick the one whose model matches how you already think about your app, and you'll spend a lot less time fighting your tooling.