Skip to content

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

SolutionTypeBest For
InngestEvent-drivenComplex workflows
Trigger.devCode-firstTypeScript workflows
QStashHTTP queueSimple async tasks
BullMQRedis queueSelf-hosted, high volume

Setup

bash
npm install inngest
typescript
// 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 init

Define 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

  1. Inngest - Best for complex workflows with steps
  2. Trigger.dev - Great DX, visual monitoring
  3. QStash - Simple HTTP-based queuing
  4. Retries - Always configure retry policies
  5. Steps - Break long tasks into resumable steps

Next: Module 12: Security & Performance →