Fix: Trigger.dev Not Working — Tasks Not Running, Runs Failing, or Dev Server Not Connecting
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Trigger.dev issues — task definition and triggering, dev server setup, scheduled tasks with cron, concurrency and queues, retries, idempotency, and deployment to Trigger.dev Cloud.
The Problem
A task is defined but triggering it does nothing:
import { task } from '@trigger.dev/sdk/v3';
export const myTask = task({
id: 'my-task',
run: async (payload: { userId: string }) => {
console.log('Running for', payload.userId);
},
});
// trigger() is called but the task never executesOr the dev server can’t connect:
npx trigger.dev dev
# Error: Could not connect to Trigger.devOr a task runs but fails with a timeout:
Run failed: Task execution timed out after 300 secondsProduction Incident: Background Jobs Stop, And Nobody Notices For Hours
Background jobs are an invisible critical path. When Trigger.dev breaks, the API still returns 200 — but welcome emails go unsent, password-reset codes never arrive, daily reports stop landing in inboxes, and webhook fan-outs to downstream services dry up. The blast radius is everything you pushed off the request path because it was “async.”
The most common post-incident report: a deploy that didn’t include npx trigger.dev deploy. The Trigger.dev runtime is a separate deploy step from your web app. Your Next.js app on Vercel ships fresh task code references, but the Trigger.dev workers in Cloud are still running last week’s bundle. New task IDs throw “task not found”, and renamed tasks silently lose their queue position. Add trigger.dev deploy to the same CI pipeline as your web deploy and fail the rollout if either step fails.
A worse incident: tasks are running, but the dev server you forgot to kill last Friday is also running on your laptop. Trigger.dev distributes runs across all connected workers, so 50% of production payloads were going to your laptop, which was offline over the weekend. Those runs sat in queued state, hit maxDuration, and got marked failed. Treat the dev server like a real worker — kill it before closing the lid, and alert on runs that sit in queued state for more than a minute.
The third incident pattern is the silent retry storm. A task with maxAttempts: 5 and exponential backoff that always fails (bad API key, deleted Postgres row) will keep retrying for hours and burn through your concurrency budget. Other tasks queue behind it. Set up dashboard alerts on retry counts and on queue depth — if either rises sharply, page on-call before the queue blocks user-facing flows.
Why This Happens
Trigger.dev v3 runs your task code in a managed runtime (not inside your Next.js server). This architecture has specific requirements:
- Tasks must be in a
trigger/directory — thetrigger.config.tstells the Trigger.dev CLI where to find tasks. Files outside the configured directory aren’t discovered. Each task file must export a task created with thetask()function. trigger.dev devruns a separate process — the dev server connects to Trigger.dev Cloud (or self-hosted) and executes tasks locally. If the connection fails (wrong API key, network issues), tasks are queued but never picked up.- Tasks run in isolated workers — unlike Inngest which calls your HTTP endpoints, Trigger.dev v3 runs your code in its own runtime. This means tasks can’t access your Next.js server’s in-memory state, but they can access databases, APIs, and file systems.
- Triggering requires the SDK — you call
tasks.trigger()ormyTask.trigger()from your application code. The trigger sends a message to Trigger.dev, which then executes the task. If the API key or project reference is wrong, the trigger silently fails.
A subtler reason for “task triggered but never ran” is the two-environment split. Trigger.dev has separate environments (typically dev and prod) with separate secret keys. If your web app uses tr_dev_... but the workers are deployed to prod, the trigger lands in the dev queue while no dev worker is online. The Trigger.dev dashboard shows the run as “queued” forever. Always assert that TRIGGER_SECRET_KEY in your web app and your deployed worker match the same environment.
Another silent failure is payload size. Trigger.dev v3 imposes payload size limits (typically a few MB). If you pass a giant base64-encoded image or a 50k-row dataset directly, the trigger call may reject the payload, and depending on SDK version, the error may be swallowed by a .catch(console.error) you forgot about. For large data, store it in S3, R2, or a database, and pass an ID to the task. The task fetches the actual data inside its runtime.
A third class of failure is the maxDuration cliff. The default v3 maxDuration is 300 seconds. A run that crosses it gets killed mid-flight with no retry — the partial work is lost, the task is marked TIMED_OUT, and side effects already committed (database writes, external API calls) remain. Always design tasks to be resumable: either chunk them into sub-tasks with triggerAndWait, checkpoint progress in your database, or write idempotent operations so a re-run is safe.
Fix 1: Set Up Trigger.dev v3
npm install @trigger.dev/sdk
npx trigger.dev init// trigger.config.ts — project configuration
import { defineConfig } from '@trigger.dev/sdk/v3';
export default defineConfig({
project: 'proj_xxxxxxxxxx', // From Trigger.dev dashboard
runtime: 'node',
logLevel: 'log',
retries: {
enabledInDev: true,
default: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
factor: 2,
},
},
dirs: ['./trigger'], // Directory containing task files
});// trigger/example.ts — define a task
import { task, logger } from '@trigger.dev/sdk/v3';
export const processOrder = task({
id: 'process-order',
// Max execution time
maxDuration: 300, // 5 minutes
run: async (payload: { orderId: string; userId: string }) => {
logger.info('Processing order', { orderId: payload.orderId });
// Step 1: Fetch order
const order = await db.query.orders.findFirst({
where: eq(orders.id, payload.orderId),
});
if (!order) {
throw new Error(`Order ${payload.orderId} not found`);
}
// Step 2: Process payment
logger.info('Charging payment', { amount: order.total });
const payment = await chargePayment(order);
// Step 3: Send confirmation
await sendOrderConfirmation(order, payment);
// Return value is stored with the run
return { orderId: order.id, paymentId: payment.id, status: 'completed' };
},
});# Start the dev server
npx trigger.dev dev
# Or with specific config
TRIGGER_SECRET_KEY=tr_dev_xxx npx trigger.dev devFix 2: Trigger Tasks from Your App
// app/api/orders/route.ts — trigger from an API route
import { tasks } from '@trigger.dev/sdk/v3';
import type { processOrder } from '@/trigger/example';
export async function POST(req: Request) {
const { orderId, userId } = await req.json();
// Trigger the task — returns immediately
const handle = await tasks.trigger<typeof processOrder>('process-order', {
orderId,
userId,
});
return Response.json({
message: 'Order processing started',
runId: handle.id, // Track the run
});
}
// Or import the task directly
import { processOrder } from '@/trigger/example';
const handle = await processOrder.trigger({ orderId: '123', userId: '456' });
// Trigger and wait for result
const result = await processOrder.triggerAndWait({ orderId: '123', userId: '456' });
console.log(result.status); // 'completed'
// Batch trigger — multiple payloads
const handles = await processOrder.batchTrigger([
{ payload: { orderId: '1', userId: '100' } },
{ payload: { orderId: '2', userId: '101' } },
{ payload: { orderId: '3', userId: '102' } },
]);Fix 3: Scheduled Tasks (Cron)
// trigger/scheduled.ts
import { schedules, logger } from '@trigger.dev/sdk/v3';
// Cron-based scheduled task
export const dailyReport = schedules.task({
id: 'daily-report',
cron: '0 9 * * *', // Every day at 9 AM UTC
run: async (payload) => {
logger.info('Generating daily report', {
timestamp: payload.timestamp,
lastTimestamp: payload.lastTimestamp,
});
const stats = await generateDailyStats();
await sendReportEmail(stats);
return { reportGenerated: true, stats };
},
});
// Cleanup old data weekly
export const weeklyCleanup = schedules.task({
id: 'weekly-cleanup',
cron: '0 3 * * 0', // Every Sunday at 3 AM
run: async () => {
const deleted = await db.delete(sessions)
.where(lt(sessions.expiresAt, new Date()))
.returning();
logger.info(`Cleaned up ${deleted.length} expired sessions`);
return { deletedCount: deleted.length };
},
});Fix 4: Concurrency and Queues
// trigger/email.ts — rate-limited task
import { task, queue } from '@trigger.dev/sdk/v3';
// Define a queue with concurrency limits
const emailQueue = queue({
name: 'email-queue',
concurrencyLimit: 5, // Max 5 emails sending at once
});
export const sendEmail = task({
id: 'send-email',
queue: emailQueue,
run: async (payload: { to: string; template: string; data: Record<string, any> }) => {
await emailProvider.send({
to: payload.to,
template: payload.template,
data: payload.data,
});
return { sent: true, to: payload.to };
},
});
// Per-key concurrency — one task per user at a time
export const syncUserData = task({
id: 'sync-user-data',
queue: {
name: 'user-sync',
concurrencyLimit: 1,
},
run: async (payload: { userId: string }) => {
// Only one sync runs per user at a time
await performSync(payload.userId);
},
});Fix 5: Retry and Error Handling
// trigger/resilient.ts
import { task, logger, retry } from '@trigger.dev/sdk/v3';
export const callExternalApi = task({
id: 'call-external-api',
retry: {
maxAttempts: 5,
factor: 2, // Exponential backoff
minTimeoutInMs: 1000, // Start at 1s
maxTimeoutInMs: 30000, // Cap at 30s
randomize: true, // Add jitter
},
run: async (payload: { url: string; data: any }) => {
const response = await fetch(payload.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload.data),
});
if (response.status === 429) {
// Rate limited — throw to trigger retry
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limited. Retry after ${retryAfter}s`);
}
if (response.status >= 500) {
// Server error — throw to trigger retry
throw new Error(`Server error: ${response.status}`);
}
if (response.status === 400) {
// Client error — abort, don't retry
logger.error('Bad request — not retrying', { status: response.status });
throw new retry.AbortTaskRunError('Bad request — invalid payload');
}
return response.json();
},
});
// Handle cleanup on failure
export const riskyTask = task({
id: 'risky-task',
onFailure: async (payload, error, params) => {
// Called after all retries are exhausted
logger.error('Task failed permanently', {
error: error.message,
attempt: params.attempt,
});
await notifySlack(`Task risky-task failed: ${error.message}`);
},
run: async (payload: { jobId: string }) => {
// Task logic
},
});Fix 6: Deploy to Production
# Deploy tasks to Trigger.dev Cloud
npx trigger.dev deploy
# Deploy to a specific environment
npx trigger.dev deploy --env production
# Check deployment status
npx trigger.dev whoami// Environment variables needed:
// TRIGGER_SECRET_KEY=tr_dev_xxx (dev)
// TRIGGER_SECRET_KEY=tr_prod_xxx (production)GitHub Actions deployment:
# .github/workflows/deploy-trigger.yml
name: Deploy Trigger.dev
on:
push:
branches: [main]
paths:
- 'trigger/**'
- 'trigger.config.ts'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx trigger.dev deploy
env:
TRIGGER_SECRET_KEY: ${{ secrets.TRIGGER_SECRET_KEY }}Still Not Working?
Task triggers but never runs — the dev server must be running (npx trigger.dev dev). Without it, triggers are queued but no worker picks them up. Check the Trigger.dev dashboard to see if the run is in “queued” or “executing” state. Also verify TRIGGER_SECRET_KEY is set correctly.
“Could not connect” during dev — check your API key and network. The dev server connects to Trigger.dev Cloud via WebSocket. Corporate firewalls or VPNs may block this. Also verify the project ID in trigger.config.ts matches your dashboard project.
Task times out at 300 seconds — increase maxDuration in the task definition. For long-running tasks, break them into smaller sub-tasks using tasks.triggerAndWait() from within a parent task. Each sub-task gets its own timeout.
Changes to task code aren’t reflected — restart npx trigger.dev dev after modifying trigger.config.ts. For task file changes, the dev server should hot-reload, but if it doesn’t, restart it. In production, you must run npx trigger.dev deploy to push changes.
Production tasks reference an old bundle — the web app deployed but npx trigger.dev deploy did not. The worker keeps running the previous bundle, so newly-added task IDs return “task not found” and renamed handlers go to dead-letter. Chain both deploys in the same CI job and fail the rollout if either step fails. In Trigger.dev’s dashboard, compare the deployed version against git rev-parse HEAD to confirm.
Queue depth keeps growing — a poison message is hogging a queue slot, or concurrency is too low for the incoming rate. Inspect the dashboard for runs that have been retrying for hours; cancel them or wrap the broken path in retry.AbortTaskRunError so failures don’t loop. Then raise the queue’s concurrencyLimit to match the burst rate, keeping in mind your downstream API’s own rate limits.
Triggers from local dev keep stealing production runs — leaving npx trigger.dev dev running with a tr_prod_... key turns your laptop into a production worker. Lock that down: never set the prod key in .env.local, and use TRIGGER_SECRET_KEY=tr_dev_... for local runs only. Trigger.dev’s environment separation is per-key, not per-machine.
For related background job and serverless issues, see Fix: Inngest Not Working, Fix: BullMQ Not Working, Fix: AWS Lambda Timeout, and Fix: Temporal Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Inngest Not Working — Functions Not Triggering, Steps Failing, or Events Not Received
How to fix Inngest issues — function and event setup, step orchestration, retries and error handling, cron scheduling, concurrency control, fan-out patterns, and local development with the Dev Server.
Fix: SST Not Working — Deploy Failing, Bindings Not Linking, or Lambda Functions Timing Out
How to fix SST (Serverless Stack) issues — resource configuration with sst.config.ts, linking resources to functions, local dev with sst dev, database and storage setup, and deployment troubleshooting.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.