
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Achieving zero-downtime deployment for Elixir requires understanding the BEAM’s unique ability to run two versions of a module simultaneously. Unlike traditional frameworks that require full process restarts, Elixir allows you to update code in a running system without dropping connections or losing state. This capability is powerful but demands precise configuration; a misconfigured release can still cause brief outages during socket handoffs or database migration windows.
How does zero-downtime deployment for Elixir actually work?
The foundation of zero-downtime deployment for Elixir lies in the Erlang/OTP platform's design. The BEAM virtual machine supports having two versions of any module loaded at once: the "current" version and the "old" version. When you perform a hot code upgrade, the system marks existing code as old and loads the new version as current. New function calls use the current version, while existing processes continue executing against the old version until they explicitly yield control via a receive loop or are migrated.
This mechanism differs fundamentally from typical web frameworks. In Node.js or Python, deploying new code means stopping the old process entirely and starting a new one, which inevitably drops in-flight requests unless you implement external orchestration. With Elixir, the runtime itself manages this transition internally. However, relying solely on hot upgrades is risky in modern containerized environments. Most production teams now combine OTP primitives with infrastructure-level rolling restarts, using the BEAM's clustering capabilities to coordinate graceful shutdowns across nodes.
For Phoenix applications specifically, you must also consider WebSocket connections and PubSub subscriptions. These long-lived processes don't automatically migrate during code swaps. Your deployment strategy needs to account for draining these connections gracefully, typically by signaling clients to reconnect after a brief delay while the backend transitions. I cover connection draining patterns extensively in my article on blue-green and canary deploys on Kubernetes, which applies directly to Elixir clusters.
How do you configure Elixir releases for safe hot upgrades?
Hot upgrades require explicit configuration in your release definition. Without proper appup files and relup scripts, the upgrade will fail or worse, silently corrupt state. Start by ensuring your mix.exs defines a valid release with version tracking enabled.
# mix.exs
def project do
[
app: :my_app,
version: "1.3.0",
releases: [
my_app: [
include_executables_for: [:unix],
applications: [runtime_tools: :permanent],
steps: [:assemble, :tar]
]
]
]
end The critical piece is generating correct appup files. For most GenServers, you need to specify how to transform state between versions. Create or edit src/my_app.appup.src:
{"1.3.0",
[{"1.2.0", [{add_module, MyApp.NewFeature},
{update, MyApp.Worker, {advanced, []}}]}],
[{"1.2.0", [{delete_module, MyApp.NewFeature},
{update, MyApp.Worker, {advanced, []}}]}]
}. The {advanced, []} tuple tells OTP to call your module's code_change/3 callback. Implement this function to handle state transformation:
def code_change(old_vsn, state, extra) do
# Transform state from v1.2 format to v1.3 format
new_state = Map.put(state, :new_field, default_value())
{:ok, new_state}
end A common mistake is forgetting that code_change runs synchronously during the upgrade. If your transformation takes too long or blocks, it can stall the entire release process. Keep transformations lightweight and defer heavy computation to async tasks. Always test upgrades locally using MIX_ENV=prod mix release.upgrade before attempting them in production. For deeper guidance on managing stateful services during transitions, see Kubernetes secrets management done right — secure secret rotation often accompanies code upgrades.
When should you use rolling restarts instead of hot upgrades?
Despite the elegance of hot upgrades, many production Elixir teams in 2026 prefer rolling restarts within Kubernetes or similar orchestrators. Hot upgrades carry inherent risk: a buggy code_change callback can leave processes in an inconsistent state that only manifests hours later. Rolling restarts trade some sophistication for predictability. Each pod terminates gracefully, finishes in-flight requests, then exits cleanly before its replacement becomes ready.
Use rolling restarts when your changes involve dependency updates, Erlang NIF modifications, or VM-level configuration changes. These cannot be handled by hot code loading. Also prefer rolling restarts if your team lacks deep OTP expertise; debugging a failed hot upgrade at 3 AM during an incident is significantly harder than debugging a standard crash loop. Configure your Kubernetes deployment with appropriate readiness probes to ensure new pods serve traffic only after completing initialization:
readinessProbe:
httpGet:
path: /health/ready
port: 4000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 4000
initialDelaySeconds: 30
periodSeconds: 10 Your Phoenix endpoint must distinguish between "alive" (process running) and "ready" (dependencies connected, caches warmed). Return 503 from /health/ready until all prerequisites are satisfied. This prevents the load balancer from routing requests to a node that hasn't finished booting. For comprehensive health check implementation patterns aligned with observability best practices, refer to the four golden signals of monitoring.
| Criteria | Hot Upgrade | Rolling Restart |
|---|---|---|
| Downtime Risk | Very low if configured correctly | Near-zero with proper probes |
| Complexity | High (appup, code_change testing) | Low (standard K8s rollout) |
| Dependency Updates | Not supported | Fully supported |
| State Preservation | In-memory state retained | Requires external persistence |
| Rollback Speed | Instant (revert to old code) | Minutes (previous image tag) |
| Best For | Bug fixes, pure logic changes | Infra changes, deps, config |
How do you handle database migrations during Elixir deployments?
Database migrations are the most common source of deployment-related downtime in Elixir applications, regardless of whether you use hot upgrades or rolling restarts. The rule is simple: never run destructive migrations concurrently with application code that expects the old schema. Adopt the expand-contract pattern for all schema changes.
- Expand: Add new columns or tables as nullable/optional. Deploy application code that writes to both old and new structures but reads from the old one. This migration is backward-compatible and safe to run before the code deploy.
- Migrate Data: Backfill existing records into the new structure using a separate background job or migration script. Monitor progress independently of application deployments.
- Contract: Once backfill completes and new code is stable, deploy code that reads from the new structure. Finally, remove deprecated columns in a subsequent release.
Never run mix ecto.migrate as part of your application startup in production. This creates a race condition where multiple nodes attempt migrations simultaneously, potentially causing lock contention or partial failures. Instead, run migrations as a dedicated init container or pre-deploy job that completes before application pods start. For PostgreSQL-specific backup and recovery procedures that complement safe migration practices, consult PostgreSQL backup and restore with pg_dump.
What monitoring validates successful zero-downtime deployments?
You cannot claim zero-downtime deployment for Elixir without instrumentation proving it. Define clear SLIs around error rate and latency during deployment windows. A truly zero-downtime deploy shows no spike in 5xx errors and no p99 latency increase beyond normal variance. Instrument your Phoenix endpoints with OpenTelemetry to capture per-request traces across the deployment boundary.
Set up deployment-aware dashboards that overlay release timestamps onto metric graphs. If you see even a single error burst correlated with a pod termination or hot upgrade event, your zero-downtime claim is invalid. Common failure points include WebSocket reconnection storms, cache cold starts on new nodes, and database connection pool exhaustion during rolling restarts. Pre-warm caches and stagger pod terminations to smooth out these transitions. Effective alerting thresholds for deployment anomalies are covered in alerting with Prometheus Alertmanager.
Implementing Reliable Zero-Downtime Deployment for Elixir
Successful zero-downtime deployment for Elixir combines BEAM-native capabilities with disciplined operational practices. Choose hot upgrades for targeted logic fixes where state preservation matters, and rolling restarts for broader changes involving dependencies or infrastructure. Always validate with real metrics, not assumptions. Test every upgrade path in staging with production-like traffic patterns before touching live systems. If your deployment still causes errors, instrument deeper rather than accepting intermittent failures as inevitable. For teams building Elixir systems in Nepal or globally, getting this right separates professional-grade platforms from fragile prototypes. Need help architecting resilient Elixir deployments? Contact me to discuss your specific requirements.