TL;DR:

  • Trigger.dev v3 replaces the worker-queue-job pattern with durable TypeScript tasks that run as isolated serverless functions — no queue server to manage.
  • Tasks support multi-hour execution with automatic checkpointing, making it practical for LLM workflows, video processing, and other long-running operations that break standard serverless timeouts.
  • Self-hostable on your own infrastructure with the open-source version; the cloud service handles orchestration so you just deploy task files and call trigger().

Background jobs in Node.js have historically meant either bolting on Bull/BullMQ with Redis, reaching for a managed queue like AWS SQS with Lambda consumers, or accepting that your Express route will time out mid-operation. None of these options feel natural to TypeScript developers who want to write type-safe code without spinning up queue infrastructure.

Trigger.dev v3 takes a different approach: tasks are just TypeScript functions exported from a special file, and the Trigger.dev runtime handles orchestration, persistence, retries, and execution. You call trigger() from your app the same way you’d call a function, but the execution happens asynchronously in a managed environment.

Core Concepts

Tasks are the unit of work. A task is a TypeScript function with an id, a run function, and optional configuration like maxDuration, retry, and queue:

// src/trigger/processUpload.ts
import { task, logger } from "@trigger.dev/sdk/v3";

export const processUpload = task({
  id: "process-upload",
  maxDuration: 300, // 5 minutes max
  retry: {
    maxAttempts: 3,
    backoffFactor: 2,
    minTimeoutInMs: 1000,
  },
  run: async (payload: { fileId: string; userId: string }) => {
    logger.info("Processing upload", { fileId: payload.fileId });

    // Your long-running work here
    const result = await processFileWithOCR(payload.fileId);
    await updateDatabaseRecord(payload.userId, result);
    await sendNotificationEmail(payload.userId);

    return { success: true, recordId: result.id };
  },
});

Triggering a task from your app:

// In your Next.js route or server action
import { processUpload } from "@/trigger/processUpload";

export async function POST(req: Request) {
  const { fileId } = await req.json();
  const userId = await getCurrentUserId();

  // Trigger the task — returns immediately with a run handle
  const handle = await processUpload.trigger({ fileId, userId });

  return Response.json({ runId: handle.id });
}

The trigger() call returns immediately. The task runs asynchronously. Your HTTP route responds in milliseconds.

Long-Running Tasks with Wait Points

The headline v3 feature is support for genuinely long task durations — up to 24 hours by default, configurable higher. This is enabled by checkpointing: the task process is snapshotted at wait points and can be resumed on a different worker later.

The wait utilities are the mechanism:

import { task, wait, logger } from "@trigger.dev/sdk/v3";

export const generateReport = task({
  id: "generate-report",
  maxDuration: 3600, // 1 hour
  run: async (payload: { reportId: string }) => {
    // Start long data aggregation
    await startDataAggregation(payload.reportId);

    // Wait up to 30 minutes for the aggregation to complete
    const aggregationResult = await wait.for({ seconds: 30 * 60 });

    // Continue after wait — may be on a different machine
    const report = await generatePDFReport(payload.reportId, aggregationResult);
    return report;
  },
});

For LLM-heavy workflows where you might be chaining multiple API calls with delays between them, this is the key capability that separates Trigger.dev from standard serverless functions.

Scheduled Tasks (Cron)

Replacing cron jobs or scheduled Lambda functions:

import { schedules } from "@trigger.dev/sdk/v3";

export const dailyDigest = schedules.task({
  id: "daily-digest",
  cron: "0 8 * * 1-5", // 8am weekdays
  run: async (payload) => {
    const yesterday = new Date(payload.timestamp);
    yesterday.setDate(yesterday.getDate() - 1);

    const metrics = await aggregateDailyMetrics(yesterday);
    await sendDigestEmail(metrics);

    logger.info("Digest sent", { date: yesterday.toISOString() });
  },
});

Scheduled tasks support the same retry and duration configuration as regular tasks. The cron expression is evaluated in UTC by default; you can specify a timezone.

Subtasks and Fan-Out Patterns

Trigger.dev handles fan-out naturally through subtasks:

import { task, logger } from "@trigger.dev/sdk/v3";

export const processUserBatch = task({
  id: "process-user-batch",
  run: async (payload: { userIds: string[] }) => {
    // Fan out: trigger a subtask per user in parallel
    const results = await processOneUser.triggerAndWait(
      payload.userIds.map((userId) => ({ payload: { userId } }))
    );

    const successful = results.filter((r) => r.ok).length;
    logger.info(`Processed ${successful}/${payload.userIds.length} users`);

    return { successful, total: payload.userIds.length };
  },
});

export const processOneUser = task({
  id: "process-one-user",
  retry: { maxAttempts: 3 },
  run: async (payload: { userId: string }) => {
    // Per-user work
    return await updateUserData(payload.userId);
  },
});

triggerAndWait fans out all the subtasks and waits for all of them to complete before continuing. Individual subtask retries happen independently — if three of fifty subtasks fail and retry, the other 47 aren’t affected.

Queue Configuration for Rate Limiting

Useful when your tasks hit rate-limited external APIs:

export const callAIApi = task({
  id: "call-ai-api",
  queue: {
    name: "ai-api-queue",
    concurrencyLimit: 5, // Max 5 concurrent executions
  },
  run: async (payload: { prompt: string }) => {
    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: payload.prompt }],
    });
    return response.choices[0].message.content;
  },
});

The queue is shared across all instances of the task. If you trigger 100 callAIApi tasks simultaneously, only 5 run concurrently; the rest queue and execute as slots open.

Setup with Next.js

npm install @trigger.dev/sdk@latest
npx trigger.dev@latest init

The init wizard creates a trigger.config.ts file and sets up the dev server connection. Add TRIGGER_SECRET_KEY to your .env.local (get it from the Trigger.dev dashboard or your self-hosted instance).

Run the dev CLI alongside Next.js:

npx trigger.dev@latest dev

This connects your local task files to the Trigger.dev runtime and handles execution. In development, tasks run locally; in production, they deploy to Trigger.dev’s managed execution environment.

Self-Hosting

Trigger.dev is open-source (AGPLv3). The self-hosted deployment uses Docker Compose with Postgres and Redis:

git clone https://github.com/triggerdotdev/trigger.dev
cd trigger.dev
cp .env.example .env
# Configure DATABASE_URL, REDIS_URL, MAGIC_LINK_SECRET, etc.
docker compose up

Self-hosting gives you full control over data residency and removes the dependency on the cloud service. The trade-off is managing the infrastructure and the platform updates yourself.

Trigger.dev vs Alternatives

FeatureTrigger.dev v3Bull/BullMQTemporal
Infrastructure neededNone (cloud) or DockerRedisTemporal server
TypeScript nativeYesPartialSDK-based
Max task duration24h+Unlimited (poll)Unlimited
Scheduled tasksBuilt-inVia agenda/node-cronBuilt-in
UI/observabilityIncludedBull Board addonIncluded
Self-hostYes (open-source)Redis onlyYes

Temporal is the more powerful option for complex durable workflow orchestration with external service interaction. BullMQ remains excellent if you already have Redis and want explicit queue semantics. Trigger.dev v3 sits in the middle: more powerful than BullMQ for long-running and fan-out tasks, simpler to operate than Temporal for most TypeScript app use cases.

For Next.js and similar apps where you want background tasks without queue infrastructure, and where TypeScript type safety across the trigger/task boundary matters, Trigger.dev v3 is the cleanest option currently available.