A dalp Helm release runs the Platform API as four roles in six Kubernetes Deployments that you size separately. The api role runs a current v2 fleet and a frozen v1 fleet, the indexer role runs serving and rebuild pods, and the runner and scheduler roles run one Deployment each. Every role reaches a PostgreSQL database through a connection pooler. Use this page to size a deployment or check its high availability: how many replicas each component starts with, how far each scales, what a failure of each costs, and what extra replicas cost at the pooler and the database. For a first install, see Prerequisites and Helm charts. For backup and restore, and for who owns recovery, see Backup and disaster recovery.
One dalp release runs four Platform API roles, three of them drawn, and every role reaches PostgreSQL through PgBouncer.
What deploys
The dalp Helm chart is the umbrella you deploy for one DALP Instance, one copy of the platform's own workloads, running in the cluster you choose and connected to the ledger networks you approve. The datastores, ingress, ledger RPC endpoints and SMTP relay are yours. It composes conditionally enabled subcharts. Five are on by default: dapi runs the Platform API, dapp the Console, docs this documentation site, pgbouncer the PostgreSQL connection pooler, and postgres-exporter the database metrics exporter. Optional components are off by default, including blockscout, a block explorer for EVM networks, and investor-portal, the Investor portal, which investorPortal.enabled switches on. Set docs. to leave the documentation site out.
The dalp chart deploys no PostgreSQL, Redis or object storage for the platform. You supply those services and point global.datastores at them, and your providers carry their backup and failover. Ingress is yours as well.
The dalp chart owns what Velero backs up: its backup block renders the storage location and schedule. The chart never deploys a Velero server.
Every dalp service reads its configuration from the chart values, with your own values overlay on top. Confirm the rendered ConfigMaps and Secrets contain the values your deployment requires.
High availability by component
The chart's defaults start the current API, the runner, the serving indexer, the Console and the connection pooler at two replicas. Each has a disruption budget that keeps one available during a voluntary disruption such as a node drain. The frozen v1 API and the scheduler start at one replica with a budget of zero, so a node drain is never refused. These numbers are a starting footprint. Size them against your own load and your own availability objective. Prerequisites lists the CPU and memory each pod requests.
Topology spread constraints apply to every Platform API role, not only the v2 fleet. The documentation site and the optional Investor portal each autoscale from 2 to 5 replicas, and the optional block explorer frontend from 1 to 5.
The indexer floor of two is deliberate. A single indexer stops all indexing for its whole boot window on every restart, and a disruption budget of one against one replica allows zero voluntary disruptions, so a node drain is refused outright. More serving replicas never add throughput for a single network: live indexing keeps exactly one owner pod per network, held by a database lease. Each replica holds the networks it claimed, and the replica count buys failover and a spread of networks across pods. If an owner fails, a surviving replica claims its networks and resumes from the last committed position.
Alerting in the observability chart is off by default and turns on with grafana.. Once it is on, its ChainUnowned alert fires when a network has had no live owner for five minutes.
Every Platform API pod is stateless. Persistence lives in PostgreSQL and object storage, with Redis as a cache and change-notification tier, not in the pod. The Console (dapp) is stateless too. A failed pod loses no data, so recovering these components is a reschedule. While the single scheduler pod is away, its singleton polls pause.
The chart provides no high availability for the stateful layer. PostgreSQL, Redis and object storage are services you supply (see Stateful dependencies), so you take their replication and failover from your provider. The platform does not orchestrate failover of a database, and its recovery point after a database failover tracks the provider's replication lag. Backup and disaster recovery lists who sets which objective.
Durable workflow execution shards work across every runner replica. The shard count (durable., default 64) is a modulus every runner and client hashes entities through, which makes it cluster-wide configuration: two participants that disagree about it address the same work to different shards. Change it everywhere at once, as one coordinated configuration change, never per pod.
Ledger endpoints
Each network's endpoint pool is a failover group of its own. With two or more endpoints in a pool, the indexer and the Platform API stop sending calls to an endpoint that lags behind the chain or fails, and keep serving from another one. A platform administrator extends that pool in the Console under Networks, and a change applies without a restart. The transaction engine, which signs and broadcasts, uses only the endpoints listed in rpc.urls in the deployment values, as Networks describes. Backup and disaster recovery describes how the platform scores and switches endpoints. A network with one endpoint has no failover, so give each production network at least two in rpc.urls, from providers that fail independently.
How queries reach PostgreSQL
Application queries reach PostgreSQL through PgBouncer, not directly. The umbrella chart enables the pgbouncer subchart by default and renders every service's database URL at the PgBouncer Service, so routing is the chart's decision end to end. No service opts in or out on its own. The database migration Job keeps a direct connection, so its schema changes run outside the pooler's transaction-mode constraints. The PostgreSQL exporter and the optional Blockscout workload also connect directly.
PgBouncer runs in transaction pooling mode, and the chart rejects every other mode. Two PgBouncer replicas each run one process, and every limit is per process: a process accepts up to 3000 client sockets by default while holding a small bounded pool of real server connections per user and database pair. The pooler is what lets replica counts scale without multiplying server connections: an extra api pod adds cheap client sockets, not PostgreSQL backends.
The budget that matters therefore lives in the pgbouncer chart's values. They price the aggregate peak, meaning every pooled alias across both peers, the reserve pools, and the direct consumers (the block explorer, the postgres-exporter, and the migration Job), against the PostgreSQL server's max_connections. If you raise a pool size, a reserve or the PgBouncer replica count without repricing, every per-process limit multiplies by the peer count and the sum spends headroom that the whole estate shares. Under saturation, clients queue instead of opening new server connections, and a client that queues past the configured wait timeout (10 seconds by default) is rejected.
The chart checks this at render time: a render fails when the aggregate exceeds the declared max_connections, or when a role's pods at their replica ceiling would hold more than 8 clients per server connection of their alias. Keep the declared max_connections equal to the server's real one, because the check can only see the declared value.
Stateful dependencies
You supply PostgreSQL, Redis and object storage and point global.datastores at them. The dalp chart deploys none of them for the platform, so the availability of each is the availability of the service you chose.
One PostgreSQL server backs several logical databases (the default database, blockscout and dapp), not one database per service. Only the active object storage provider's configuration is used. Backup and disaster recovery lists who owns the backup and failover of each.
When the cache and notification transport fail
The platform keeps serving while Redis is away, but the default multi-replica deployment needs a shared Redis, as Prerequisites says, and the dalp chart enables the Redis cache backend by default. A pod does not wait for Redis before it starts serving, because gating startup on it turns a brief interruption into a restart loop across every role. A pod that starts before Redis is reachable serves from its in-process tier and reconnects on its own.
Each pod supervises its cache and notification connections and retries on a bounded backoff with no limit on the number of attempts. Reconnection also checks that Redis is serving requests, not only accepting connections, so an endpoint that is loading a snapshot or failing over is retried rather than treated as healthy.
While a pod is disconnected, it cannot receive shared cache updates or invalidation notifications. Change notifications also stop, and each listener falls back to its own fixed reconcile bound instead: a lost cache-invalidation hint heals within cache., 600,000 milliseconds (ten minutes) by default, and chain-health and network-configuration reloads heal within a fixed sixty seconds. These intervals bound scheduled reconciliation, not event delivery. Dependency outages and processing delays can extend the time until fresh state is visible. Operations that promise deployment-wide reach, such as clearing a cache namespace, fail while Redis is unreachable instead of reporting a success they cannot deliver.
dalp_ is zero while a pod's Redis connection is usable. During an outage, it reports how long the connection has been unavailable. Once alerting is turned on in the observability chart, its Redis alert fires for a sustained outage, which separates a brief Redis restart from a pod that is not recovering.
One chart for every environment
Every environment, from a first test install to production, installs the same dalp chart with its own values overlay. Point that overlay at your own PostgreSQL, Redis and object storage, and at your own ingress controller. See Stateful dependencies.
global. names the environment and takes one value from a closed set: local, development, test, staging, demo or production. The dapi subchart's ConfigMap derives the platform.environment setting from that value at render time, so platform.environment is not a key you set.
The observability chart, which collects metrics, logs and traces, installs as its own release beside dalp. The dalp chart does not pull it in as a dependency.
On OpenShift, layer values-openshift.yaml, a security-context overlay for the same dalp chart templates, and the observability chart's own values-openshift.yaml over its release. See OpenShift.
Deployment identity
Every deployment carries two identity values: global.dalpInstance names the instance, and global. names its environment from that closed set. Both default to local, and every non-local deployment must override them: they flow into telemetry labels and dashboards, so an instance that keeps the defaults cannot be told apart from any other in shared observability views. The dapi subchart, on by default in every dalp install, validates both: it requires each value present, checks global.dalpInstance against the 1 to 63 character lowercase slug rule, and checks global. against the same closed set, failing the render before any pod starts. The observability chart, where you install it, adds one more guard: a non-local environment that still carries the reserved local instance slug also fails the render.
Consequences and reversibility
Scaling a role is a rollout. Replica changes move no data because every Platform API pod is stateless. Scaling back requires another ownership and readiness transition. Raising pool ceilings without checking the connection budget can overload PostgreSQL, not only the role you scaled, so the pricing check has to happen alongside the replica change, not after it.
Keep at least two indexer replicas for restart headroom and voluntary node drains. Scaling changes network ownership and readiness, so verify indexing after the transition.
The shard count is the one value with no gradual path: every runner and client must agree on it, so it changes everywhere at once or not at all.
Each role's shutdown window is bounded by the chart's own numbers: a 50-second termination grace period, a 5-second preStop pause, and a 44-second DAPI_SHUTDOWN_BUDGET_MS that fits inside what remains. The budget covers the 20-second HTTP drain that DAPI_ bounds, the 15-second durable entity termination wait, and the 5-second telemetry flush. An api pod drains in-flight HTTP work within its drain timeout. A runner lets the cluster relocate its shards during the window. An indexer releases its network ownership so a surviving replica can claim the networks it owned.
Frequently asked questions
Can the runner role pick up HTTP traffic if the api role is overloaded?
No. The runner's HTTP port is health-only: kubelet probes it, but the Service that routes application traffic never selects runner pods. Scaling the api role, not the runner role, is what adds HTTP capacity, and the two autoscalers stay independent so a workflow backlog on runner cannot starve the pool serving requests.
How much warning do I get before PgBouncer starts rejecting queued clients?
There is no guaranteed warning interval. Once alerting is turned on in the observability chart, its PgBouncerClientMaxWait alert uses a one-second queue-wait threshold, a ten-second hold, and a ten-second evaluation interval. The hold equals the 10-second query-wait timeout, so the alert fires as clients begin to be rejected, and notification delays come on top. Use the alert to investigate sustained pressure, not as a promise of time to prevent an individual rejection.
Why does PgBouncer scale by adding replicas instead of raising the number of processes in each pod?
Each PgBouncer pod runs one process by chart default, and that pin is deliberate. The pod's metrics exporter holds a single admin-console connection. A multi-process pod would route that connection to only one process at random, leaving the other processes' queue depth invisible to the saturation alert: clients could then hit the query wait timeout while monitoring reports no queue at all. Adding replicas instead keeps every process visible, because each replica carries its own exporter sidecar.
What happens if global.dalpInstance or global.deploymentEnvironment is left unset entirely, rather than kept at its default?
An unset value fails differently from the default. The dapi subchart's ConfigMap declares both values with Helm's required function, so an unset value fails that render immediately with a message naming the missing key. The observability chart, where you install it, repeats the same required guards, though there the environment check sits behind several unrelated Alloy credential checks, so a misconfigured Alloy receiver can fail that render first. Keeping global.dalpInstance at its default local while setting a non-local environment, by contrast, satisfies every presence check. Only the observability chart, where you install it, rejects that combination. The dalp chart alone renders it.
Which PostgreSQL, Redis and object storage does a deployment use?
The ones you supply. The umbrella chart's services read their connection details from values.yaml (global.datastores) regardless of who runs the backing service. When you change the PostgreSQL server, reprice the PgBouncer aggregate against the new server's own max_connections.