Module 11: Background Jobs
Handle async tasks, scheduled jobs, and long-running processes.
When You Need Background Jobs
- Email sending - Don't block user requests
- Image processing - Resize, compress uploads
- Data sync - Sync with third-party APIs
- Reports - Generate PDFs, exports
- Scheduled tasks - Daily cleanup, weekly reports
- Webhooks - Process incoming webhooks reliably
Solution Comparison
| Solution | Type | Best For |
|---|---|---|
| Inngest | Event-driven | Complex workflows |
| Trigger.dev | Code-first | TypeScript workflows |
| QStash | HTTP queue | Simple async tasks |
| BullMQ | Redis queue | Self-hosted, high volume |
Inngest (Recommended)
Setup
bash
npm install inngesttypescript
// lib/inngest/client.ts
import { Inngest } from 'inngest'
export const inngest = new Inngest({
id: 'my-saas',
schemas: new EventSchemas().fromRecord<Events>(),
})
type Events = {
'user/created': { data: { userId: string; email: string } }
'email/send': { data: { to: string; subject: string; body: string } }
}Define Functions
typescript
// lib/inngest/functions.ts
import { inngest } from './client'
import { db } from '@/lib/db'
import { sendEmail } from '@/lib/email'
// Simple function
export const sendWelcomeEmail = inngest.createFunction(
{ id: 'send-welcome-email' },
{ event: 'user/created' },
async ({ event }) => {
await sendEmail({
to: event.data.email,
subject: 'Welcome!',
body: 'Thanks for signing up.',
})
}
)
// Function with retries and steps
export const processSignup = inngest.createFunction(
{
id: 'process-signup',
retries: 3,
},
{ event: 'user/created' },
async ({ event, step }) => {
// Step 1: Create database records
const user = await step.run('create-user-record', async () => {
return db.user.create({
data: { id: event.data.userId, email: event.data.email },
})
})
// Step 2: Send welcome email (runs after step 1)
await step.run('send-welcome-email', async () => {
await sendEmail({
to: user.email,
subject: 'Welcome!',
body: 'Thanks for signing up.',
})
})
// Step 3: Notify team (can be parallel with step 2)
await step.run('notify-team', async () => {
await sendSlackMessage(`New signup: ${user.email}`)
})
return { success: true }
}
)
// Scheduled function (cron)
export const dailyCleanup = inngest.createFunction(
{ id: 'daily-cleanup' },
{ cron: '0 0 * * *' }, // Midnight daily
async () => {
// Delete old sessions
await db.session.deleteMany({
where: {
expiresAt: { lt: new Date() },
},
})
}
)API Route
typescript
// app/api/inngest/route.ts
import { serve } from 'inngest/next'
import { inngest } from '@/lib/inngest/client'
import {
sendWelcomeEmail,
processSignup,
dailyCleanup,
} from '@/lib/inngest/functions'
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [sendWelcomeEmail, processSignup, dailyCleanup],
})Trigger Events
typescript
// In your application code
import { inngest } from '@/lib/inngest/client'
// After user signs up
await inngest.send({
name: 'user/created',
data: {
userId: user.id,
email: user.email,
},
})Trigger.dev
Setup
bash
npx trigger.dev@latest initDefine Jobs
typescript
// jobs/welcome-email.ts
import { client } from '@/trigger'
import { eventTrigger } from '@trigger.dev/sdk'
import { z } from 'zod'
client.defineJob({
id: 'send-welcome-email',
name: 'Send Welcome Email',
version: '1.0.0',
trigger: eventTrigger({
name: 'user.created',
schema: z.object({
userId: z.string(),
email: z.string().email(),
}),
}),
run: async (payload, io) => {
// Log progress
await io.logger.info('Sending welcome email', { email: payload.email })
// Send email
await io.sendEmail('send-email', {
to: payload.email,
subject: 'Welcome!',
body: 'Thanks for signing up.',
})
return { success: true }
},
})QStash (Simple HTTP Queue)
For simple fire-and-forget HTTP calls.
typescript
// lib/qstash.ts
import { Client } from '@upstash/qstash'
const qstash = new Client({
token: process.env.QSTASH_TOKEN!,
})
// Queue a request
await qstash.publishJSON({
url: 'https://my-app.com/api/process-webhook',
body: { webhookId: '123' },
retries: 3,
})
// Delayed execution
await qstash.publishJSON({
url: 'https://my-app.com/api/send-reminder',
body: { userId: 'user_123' },
delay: 3600, // 1 hour
})
// Scheduled (cron)
await qstash.schedules.create({
cron: '0 9 * * *', // 9am daily
destination: 'https://my-app.com/api/daily-digest',
})Patterns
Webhook Processing
typescript
// app/api/webhooks/stripe/route.ts
import { inngest } from '@/lib/inngest/client'
export async function POST(req: Request) {
const event = await req.json()
// Acknowledge immediately
// Process async via background job
await inngest.send({
name: 'stripe/webhook',
data: event,
})
return new Response('OK', { status: 200 })
}Batch Processing
typescript
const batchProcessor = inngest.createFunction(
{ id: 'batch-processor' },
{ event: 'batch/process' },
async ({ event, step }) => {
const items = event.data.items
// Process in chunks of 10
const chunks = chunkArray(items, 10)
for (let i = 0; i < chunks.length; i++) {
await step.run(`process-chunk-${i}`, async () => {
await Promise.all(chunks[i].map(processItem))
})
}
}
)Long-Running with Progress
typescript
const generateReport = inngest.createFunction(
{ id: 'generate-report' },
{ event: 'report/generate' },
async ({ event, step }) => {
// Update progress as we go
await step.run('fetch-data', async () => {
await updateProgress(event.data.reportId, 25)
return fetchReportData()
})
await step.run('process-data', async () => {
await updateProgress(event.data.reportId, 50)
return processData()
})
await step.run('generate-pdf', async () => {
await updateProgress(event.data.reportId, 75)
return generatePDF()
})
await step.run('upload', async () => {
await updateProgress(event.data.reportId, 100)
return uploadToStorage()
})
}
)Summary
- Inngest - Best for complex workflows with steps
- Trigger.dev - Great DX, visual monitoring
- QStash - Simple HTTP-based queuing
- Retries - Always configure retry policies
- Steps - Break long tasks into resumable steps