Trigger.dev started as a developer-friendly Zapier alternative and pivoted to become a TypeScript-native orchestration platform for long-running background tasks. The v2 rewrite positions it as a Temporal alternative, trading polyglot flexibility for TypeScript ergonomics and a simpler mental model. The shift reveals what developers building agent workflows actually need: durable execution, automatic retries, and observability without the operational weight of running Temporal clusters.
Trigger.dev is code-first. You write tasks in TypeScript, deploy them as code, and the platform handles queuing, state persistence, and failure recovery. The architecture choices matter for anyone building multi-step agent workflows, scheduled jobs, or event-driven pipelines that must survive transient failures.
The v1 to v2 Pivot: Events to Durable Execution
Version 1 used an event-driven model. You defined workflows that reacted to webhooks or API events. The platform routed events to your code, but execution guarantees were weak. If a step failed mid-workflow, you wrote your own retry logic and state checkpointing.
Version 2 adopted a durable execution model inspired by Temporal. Each task runs as a resumable unit. The platform checkpoints state after every step. If a worker crashes, the task resumes from the last checkpoint on a different worker. You get exactly-once semantics without writing idempotency keys.
Key architectural changes in v2:
- Task definitions replace event handlers
- Automatic state persistence after each
await - Built-in retry policies with exponential backoff
- Execution traces stored for every run
- Workers poll a central queue instead of receiving pushed events
The trade-off: v1 was lighter for simple event routing. v2 adds overhead for state serialization but handles complex workflows with partial failures.
State Persistence and Execution Guarantees
Trigger.dev checkpoints task state by serializing the execution context after each asynchronous boundary. When you await an API call, a database query, or a child task, the platform writes the current state to PostgreSQL. If the worker dies, the next worker loads the checkpoint and continues from that point.
What gets persisted:
- Function arguments and local variables in scope
- Return values from completed steps
- Pending promises and their resolution state
- Retry attempt counters and backoff timers
What does not get persisted:
- Open network connections
- File handles or streaming responses
- In-memory caches or global state
This means you cannot rely on long-lived connections across retries. Each retry starts with a fresh execution context. If you open a WebSocket, checkpoint, and retry, the socket is gone. You must design tasks to be resumable from any checkpoint.
The platform uses PostgreSQL for state storage. Each task run gets a row with a JSONB column holding the serialized state. High-throughput workloads hit PostgreSQL hard. The team added connection pooling and read replicas, but state writes remain a bottleneck for tasks with many small steps.
Retry Logic and Failure Modes
Trigger.dev retries failed tasks automatically. You configure retry policies per task: max attempts, backoff strategy, and which errors to retry.
// Example retry configuration based on Trigger.dev v2 patterns
export const fetchUserData = task({
id: "fetch-user-data",
retry: {
maxAttempts: 5,
factor: 2,
minTimeout: 1000,
maxTimeout: 60000,
randomize: true,
},
run: async (payload: { userId: string }) => {
const response = await fetch(`/api/users/${payload.userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
});
The retry policy uses exponential backoff with jitter. The first retry waits 1 second, the second waits 2 seconds, the third waits 4 seconds, and so on, up to 60 seconds. Jitter prevents thundering herds when many tasks fail simultaneously.
Failure modes to consider:
| Error Category | Behavior | Recovery |
|---|---|---|
| Transient network error | Automatic retry with backoff | Succeeds on retry |
| Invalid input data | Retries exhaust, task marked failed | Manual intervention or dead letter queue |
| Worker crash mid-step | Task resumes from last checkpoint | Transparent to caller |
| Database unavailable | Retry queue blocks, tasks wait | Resumes when DB recovers |
| Timeout exceeded | Task killed, marked failed | Configure longer timeout or split task |
The platform does not retry non-retryable errors by default (like 400 Bad Request). You can override this by catching errors and throwing a custom retryable exception.
Observability: Traces, Logs, and Debugging
Every task execution generates a trace. The dashboard shows a timeline of steps, their durations, and any errors. Each step is a span in the trace. You see exactly where time was spent and which step failed.
What the trace exposes:
- Step-by-step execution flow
- Wall-clock duration for each step
- Retry attempts and backoff delays
- Input and output payloads (truncated for large data)
- Stack traces for exceptions
Logs are structured JSON. Each log line includes the task ID, run ID, and step ID. You can filter logs by task or trace them back to a specific execution. The platform stores logs in PostgreSQL alongside state, so high-volume logging impacts database performance.
For debugging, you can replay a failed run locally. The platform serializes the input payload and retry state. You load the checkpoint in your local environment and step through the code. This works well for deterministic failures but breaks if the failure depends on external state (like a third-party API returning different data).
Concurrency and Queue Management
Trigger.dev uses a task queue backed by PostgreSQL. Workers poll the queue for pending tasks. You configure concurrency limits per task or globally per environment.
Concurrency controls:
- Per-task concurrency: Limit how many instances of a specific task run simultaneously
- Global concurrency: Cap total tasks across all types
- Queue priority: High-priority tasks jump the queue
- Rate limiting: Throttle tasks to avoid overwhelming downstream APIs
The queue uses PostgreSQL FOR UPDATE SKIP LOCKED to distribute tasks across workers without contention. Each worker claims a batch of tasks, processes them, and returns for more. If a worker dies, unclaimed tasks remain in the queue for the next worker.
Scaling behavior:
- Adding workers increases throughput linearly until PostgreSQL becomes the bottleneck
- State writes and queue polling both hit the database
- Read replicas help with queue polling but not state writes
- Beyond moderate scale, you need connection pooling and query optimization
The team is exploring Redis or a dedicated queue service for higher throughput, but the current PostgreSQL-backed design keeps operational complexity low.
Deployment Shape and Hosting Options
Trigger.dev offers a managed cloud platform and a self-hosted option. The cloud version runs on AWS with workers in multiple regions. You deploy tasks by pushing code to a Git repository. The platform builds a Docker image, deploys it to ECS, and routes tasks to the new version.
Self-hosting requirements:
- PostgreSQL database for state and queue
- Redis for caching and pub/sub (optional but recommended)
- Docker or Kubernetes for worker containers
- S3-compatible storage for logs and artifacts
The self-hosted setup uses Docker Compose for local development and Helm charts for Kubernetes. You manage scaling, monitoring, and backups. The cloud version handles this for you but locks you into their infrastructure.
Security boundaries:
- Tasks run in isolated containers with no shared memory
- Secrets are injected as environment variables at runtime
- API keys for external services are stored encrypted in PostgreSQL
- Workers authenticate to the control plane with short-lived tokens
The isolation model prevents one task from accessing another’s state or secrets. However, all tasks in the same environment share the same PostgreSQL database, so a SQL injection in one task could theoretically leak data from another. The platform sanitizes inputs, but defense in depth requires parameterized queries. Avoid using user input directly in SQL queries or task identifiers without validation.
Comparison: Trigger.dev vs. Temporal vs. Prefect
| Feature | Trigger.dev | Temporal | Prefect |
|---|---|---|---|
| Language support | TypeScript only | Go, Java, Python, TypeScript | Python, TypeScript (beta) |
| State persistence | PostgreSQL | Cassandra, MySQL, PostgreSQL | PostgreSQL, SQLite |
| Deployment | Managed cloud or self-hosted | Self-hosted (complex) | Managed cloud or self-hosted |
| Learning curve | Low (familiar async/await) | High (workflow DSL, versioning) | Medium (decorators, flows) |
| Observability | Built-in dashboard | Temporal UI (requires setup) | Prefect Cloud UI |
| Scaling ceiling | Moderate (PostgreSQL-bound) | Very high (10,000+ tasks/sec) | High (5,000+ tasks/sec) |
Trigger.dev wins on simplicity. You write normal TypeScript functions. Temporal requires learning a workflow DSL and managing versioning across deployments. Prefect sits in the middle with Python decorators and a more opinionated flow model.
The trade-off: Temporal scales further and supports polyglot workflows. If you need to orchestrate Python ML models, Go services, and Java batch jobs in one workflow, Temporal is the only option. Trigger.dev locks you into TypeScript but removes the operational burden of running Temporal clusters.
When to Use Trigger.dev
Good fit:
- TypeScript-native teams building background jobs or agent workflows
- Long-running tasks that need retries and observability (API integrations, data pipelines)
- Teams that want durable execution without operating Temporal
- Workflows with moderate throughput (hundreds of tasks per second)
Poor fit:
- Polyglot workflows mixing Python, Go, and Java
- Ultra-high-throughput systems (thousands of tasks per second)
- Teams that need full control over state storage and queue infrastructure
- Workflows requiring custom execution semantics (like speculative execution or distributed transactions)
Trigger.dev shines when you need Temporal-like guarantees without the operational complexity. It falters when you hit PostgreSQL scaling limits or need language flexibility.
Technical Verdict
Trigger.dev is a pragmatic orchestration platform for TypeScript developers who need durable execution without running Temporal. The v2 architecture handles retries, state persistence, and observability well for moderate-scale workflows. The PostgreSQL-backed queue and state store simplify operations but cap throughput at moderate scale.
Use it when you are building agent systems, scheduled jobs, or event-driven pipelines in TypeScript and want automatic retries with minimal code. Avoid it if you need polyglot workflows, ultra-high throughput, or custom execution semantics. The TypeScript-only constraint is both its strength (simplicity) and its weakness (flexibility).
For teams already invested in Temporal, Trigger.dev is not a replacement. For teams evaluating orchestration options, it is a lower-friction entry point with a clear upgrade path if you outgrow it.
Source Links
- Trigger.dev
- Show HN: Developer-first Zapier alternative (745 points, 190 comments)
- GitHub Repository