Trigger.dev started as a Zapier alternative (745 points, February 2023), then pivoted to a Temporal alternative for TypeScript developers (172 points, October 2023). The shift matters because it signals a second-generation approach to workflow orchestration: code-first durable execution without adopting Temporal’s Go runtime, event sourcing model, or gRPC protocol.
This article examines the plumbing. How does Trigger.dev implement durable execution, retries, and long-running tasks in TypeScript? What are the tradeoffs in developer experience, deployment complexity, and operational guarantees compared to Temporal?
Architecture: Durable Execution Without Event Sourcing
Temporal uses event sourcing. Every workflow decision is logged as an immutable event, replayed on resume, and stored in a history database. Trigger.dev uses a simpler model: checkpoint-based state persistence.
Trigger.dev’s execution model:
- Tasks are TypeScript functions decorated with
task(). - State is serialized at explicit checkpoints (before network calls, retries, or waits).
- The runtime stores checkpoints in Postgres, not an append-only event log.
- On resume, the runtime rehydrates state from the last checkpoint and continues.
Why this matters:
- No replay semantics. You can use non-deterministic code (random numbers, Date.now()) without breaking resumption.
- Simpler mental model. You write async functions, not workflow definitions with strict determinism rules.
- Lower storage overhead. Checkpoints replace full event histories.
Tradeoff:
- Less auditability. You lose the immutable event log that Temporal provides for debugging and compliance.
- Replay-based time travel is harder. Temporal can replay workflows from any point in history; Trigger.dev resumes from the last checkpoint.
Retry and Failure Handling
Temporal workflows are deterministic and replay-safe. Retries happen at the activity level, with exponential backoff configured in code. Trigger.dev tasks are non-deterministic, so retries happen at the task level with configurable strategies.
Trigger.dev retry primitives:
export const processOrder = task({
id: "process-order",
retry: {
maxAttempts: 5,
factor: 2,
minTimeout: 1000,
maxTimeout: 60000,
randomize: true
},
run: async ({ orderId }: { orderId: string }) => {
const order = await db.orders.findUnique({ where: { id: orderId } });
await stripe.charges.create({ amount: order.total, currency: "usd" });
await sendEmail({ to: order.email, template: "receipt" });
return { status: "completed" };
}
});
Retry behavior:
- Retries are automatic on unhandled exceptions.
- Backoff is configurable per task, not per step.
- Idempotency is your responsibility. Trigger.dev does not deduplicate retries.
Temporal comparison:
| Feature | Trigger.dev | Temporal |
|---|---|---|
| Retry scope | Task-level | Activity-level |
| Idempotency | Manual (use external keys) | Built-in (activity IDs) |
| Backoff config | Per-task decorator | Per-activity options |
| Determinism | Not required | Required for workflows |
| Replay safety | Checkpoint-based | Event-sourced |
Long-Running Tasks and Timeouts
Temporal workflows can run for years. Activities have configurable timeouts (start-to-close, schedule-to-start, heartbeat). Trigger.dev tasks can run indefinitely, but timeout behavior is simpler.
Trigger.dev timeout model:
- No global workflow timeout. Tasks run until completion or failure.
- Heartbeat timeouts are not built-in. You implement keepalive logic manually.
- Long-running tasks checkpoint periodically to avoid losing progress.
Example: multi-hour video processing
export const processVideo = task({
id: "process-video",
run: async ({ videoUrl }: { videoUrl: string }) => {
const chunks = await splitVideo(videoUrl);
for (const chunk of chunks) {
// Checkpoint before each expensive operation
await wait.for({ seconds: 1 }); // Forces checkpoint
const transcoded = await transcodeChunk(chunk);
await uploadChunk(transcoded);
}
return { status: "done", chunks: chunks.length };
}
});
Key difference:
- Temporal’s heartbeat mechanism detects stuck activities and retries them.
- Trigger.dev relies on task-level retries and external monitoring.
State Management and Observability
Temporal stores workflow state in a dedicated history database (Cassandra or Postgres). Trigger.dev stores task state in your application’s Postgres database.
Trigger.dev state schema:
taskstable: task definitions, retry config, concurrency limits.runstable: execution state, checkpoints, logs, output.eventstable: task lifecycle events (started, completed, failed).
Observability:
- Real-time task monitoring via dashboard.
- Structured logs with trace IDs.
- No distributed tracing out of the box (you add OpenTelemetry).
Temporal comparison:
- Temporal provides built-in tracing, metrics, and workflow history UI.
- Trigger.dev requires you to wire up observability yourself.
Deployment Shape
Temporal requires:
- A Go-based server cluster (frontend, history, matching, worker services).
- gRPC for client-server communication.
- A separate worker process per language SDK.
Trigger.dev requires:
- A Node.js runtime (Bun or Node 18+).
- A Postgres database.
- Optional: a separate worker process for background tasks.
Self-hosted deployment:
# docker-compose.yml
services:
trigger:
image: trigger.dev/engine:latest
environment:
DATABASE_URL: postgres://user:pass@db:5432/trigger
SECRET_KEY: your-secret-key
ports:
- "3000:3000"
db:
image: postgres:15
environment:
POSTGRES_DB: trigger
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
Managed cloud:
- Trigger.dev Cloud handles infrastructure, scaling, and observability.
- You deploy tasks as TypeScript functions; the platform runs them.
Concurrency and Queues
Temporal uses task queues and worker pools. Trigger.dev uses concurrency limits and priority queues.
Trigger.dev concurrency model:
export const sendEmail = task({
id: "send-email",
queue: {
name: "email",
concurrencyLimit: 10 // Max 10 concurrent executions
},
run: async ({ to, subject, body }) => {
await smtp.send({ to, subject, body });
}
});
Queue behavior:
- Tasks in the same queue respect the concurrency limit.
- Priority is configurable per task invocation.
- No built-in rate limiting (you implement it with delays or external tools).
Security Boundaries
Temporal workflows run in isolated worker processes. Trigger.dev tasks run in the same Node.js process by default.
Isolation model:
- Tasks share the same runtime and memory space.
- No sandboxing between tasks.
- Secrets are injected via environment variables or a secrets manager.
Risk:
- A malicious or buggy task can crash the entire worker process.
- No built-in multi-tenancy. You isolate tenants at the deployment level.
Failure Modes
Trigger.dev failure scenarios:
- Database unavailable: Tasks cannot checkpoint or resume. Executions are lost.
- Worker crash: In-flight tasks restart from the last checkpoint.
- Non-idempotent retry: Duplicate charges, emails, or API calls.
- Checkpoint bloat: Large task state increases storage and resume latency.
Mitigation:
- Use external idempotency keys (Stripe idempotency tokens, database unique constraints).
- Monitor checkpoint size and refactor tasks that accumulate too much state.
- Run multiple worker processes for redundancy.
When to Use Trigger.dev
Good fit:
- You write TypeScript and want durable execution without learning Temporal’s determinism rules.
- You need long-running tasks (hours, not days) with retries and observability.
- You want to self-host on a single Postgres database without a Go cluster.
- You are building agent workflows that checkpoint between tool calls.
Bad fit:
- You need years-long workflows with full event history for compliance.
- You require strict determinism and replay-based debugging.
- You need built-in multi-tenancy and sandboxing.
- You already run Temporal and want to avoid rewriting workflows.
Technical Verdict
Trigger.dev trades Temporal’s event sourcing and determinism guarantees for a simpler TypeScript-native developer experience. The checkpoint-based model works well for agent workflows, media processing, and background jobs that run for hours, not years. The lack of built-in idempotency and sandboxing means you must handle those concerns yourself.
Use Trigger.dev when you want durable execution without the operational overhead of a Go-based orchestrator. Avoid it when you need immutable audit logs, replay-based debugging, or workflows that span months.