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
| Feature | Trigger.dev v3 | Bull/BullMQ | Temporal |
|---|---|---|---|
| Infrastructure needed | None (cloud) or Docker | Redis | Temporal server |
| TypeScript native | Yes | Partial | SDK-based |
| Max task duration | 24h+ | Unlimited (poll) | Unlimited |
| Scheduled tasks | Built-in | Via agenda/node-cron | Built-in |
| UI/observability | Included | Bull Board addon | Included |
| Self-host | Yes (open-source) | Redis only | Yes |
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.