Configure shared storage, Redis, and database capacity before adding replicas. Measure your execution workload before choosing memory limits or autoscaling targets.
Prerequisites before scaling past one replica
You also need shared object storage — local-disk storage is per-pod, so a file uploaded through one replica is invisible to the others. See Object Storage.
What scales how
Scaling the app
The resources block above is the chart's default — it is shown so the numbers are visible, not because it changes anything. Only replicaCount needs setting unless you are raising the ceiling.
The app orchestrates workflows, and local JavaScript runs in Node child workers inside its container. Those workers and in-memory file parsing share the pod's memory limit. OOMKills can terminate in-flight runs.
Measure steady and peak memory with representative files, execution concurrency, and sandbox providers. The chart's 4 GiB request and 8 GiB limit are defaults, not measured capacity guarantees. Raise the limit or reduce concurrent work when your workload exceeds it; also measure CPU rather than assuming memory is always the bottleneck.
A PodDisruptionBudget is created automatically once replicaCount > 1, or once autoscaling.enabled with minReplicas > 1 (maxUnavailable: 25%). The app and realtime each consider their own replicaCount, but the autoscaling term is shared: minReplicas > 1 creates both PDBs, including realtime's when autoscaling.realtime.enabled is false and no realtime HPA exists. Tighten it with podDisruptionBudget.minAvailable if you need to.
Autoscaling
Requires metrics-server. When enabled, the chart omits spec.replicas so the HPA owns replica count.
Realtime gets the same HPA unless you disable it — and again, only scale it past one replica with Redis configured:
Database
Database connection and storage capacity must support all app and realtime replicas.
Connections
Each process can open more than one database pool. Budget the sum of active pools across replicas and workers against PostgreSQL's max_connections, with headroom for migrations and administration.
App subpools are created lazily, so a web process using all five can open up to 37 primary connections in addition to its configured read-replica pool. Count the actual pools and URLs used by each process. CronJob pods make HTTP calls to the app; they do not open their own PostgreSQL pools.
A pooler such as PgBouncer can separate app replica count from database connection count. Before using transaction pooling, verify the application's session-lock requirements and give the migrator a direct, non-pooled connection. Point the chart at the chosen endpoint with externalDatabase.enabled: true, externalDatabase.host, and externalDatabase.port, alongside postgresql.enabled: false. Both halves are required — with postgresql.enabled: false and externalDatabase.enabled still false the chart renders an empty DATABASE_URL. Setting app.env.DATABASE_URL does nothing — the chart computes DATABASE_URL itself and inlines it on the container, where it overrides anything from the Secret. Set the pooler's server-connection budget for your database and verify the resulting workload before increasing replicas.
Read replicas
Heavy read paths — log listing, audit logs, dashboard aggregations — can be offloaded:
Reads fall back to the primary when unset. Per-role overrides exist if different components should use different replicas:
Read replicas can lag. Recently written logs may briefly be absent from reads routed to a replica. Monitor replication lag and use the primary where immediate visibility is required.
Sizing
These are starting estimates for a managed database, not workload benchmarks:
Embeddings, logs, and table data grow with usage and retention, not just member count. Measure query latency and data growth; configure storage growth and alerts for the selected database service.
Execution concurrency
SCHEDULE_EXECUTION_CONCURRENCY_LIMIT (default 30) bounds scheduled executions across the default database-backed queue. The other three *_EXECUTION_CONCURRENCY_LIMIT variables apply only to Trigger.dev and are inert on a default self-host — see Background Jobs.
Adding replicas does not multiply this shared cap. If the queue is full, consider raising it only with memory and database headroom. If memory is exhausted, reduce concurrency or add capacity before raising the limit.
Rate limits and quotas
Billing-disabled self-hosted deployments run without plan limits by default. Plan rate limits, execution timeouts, and table/storage caps can be opted back in individually. Fixed security, request-size, and admission controls still apply; the variable list and suggested values are in Environment Variables.
An execution timeout is worth setting even on an otherwise unlimited deployment — it is what stops a runaway workflow from holding a sandbox indefinitely.
Reference topology
An illustrative topology with three minimum app replicas, two static realtime replicas, and a shared database pooler follows. It is not a throughput guarantee. With the shared HPA enabled, realtime also uses minReplicas: 3 unless you set autoscaling.realtime.enabled: false.
Plus: managed Postgres with PITR, managed Redis in an HA tier, object storage with versioning, and images pinned to an explicit tag.