Why this matters
How to deploy without downtime is mostly about making old and new code coexist for a few minutes. A full outage is easy to spot. The harder failure is a deploy where some requests hit an instance that is not ready, or an old instance that has already closed its database connections. Users see timeouts, failed payments, or empty carts.
Zero downtime does not mean zero errors. It means no user-visible break caused by the release. You need spare capacity, health checks that reflect real work, and a shutdown path that finishes requests before the process exits. The database and static assets matter as much as the app server. Aim to serve traffic from two versions at once, then move traffic in small steps and stop when the signals turn bad.
Before you start
Write down where state lives: sessions, carts, uploaded files, background jobs, cache keys, and database schema. If a deploy changes any of those, plan how the old version and new version both work. Check that your load balancer or reverse proxy supports health checks and connection draining. Confirm you have at least one extra instance of capacity so a rolling deploy does not overload the rest.
Baseline the user path before you touch anything. Pick one flow that matters, such as sign in, add to cart, checkout, or submit a form. Record the current error rate and response time in your monitoring tool. You will compare against that during the deploy. Also list external calls with short timeouts and retries, because they often turn a small deploy into a cascade.
Agree on database rules. Add columns as nullable or with defaults, backfill in batches, and keep old columns until no running code reads them. Never drop or rename a column in the same release that stops using it. For large tables, run migrations separately from the app deploy so locks do not stall traffic.
How to do it
Treat the deploy as a traffic shift. Put the new version on a small set of instances, send real traffic to them, and watch the signals that matter. Keep the old version running until the new one has handled enough real requests. This works with rolling deploys, blue-green, or canary, but the mechanics below apply to all of them.
The order matters. Fix shutdown and health checks before you add fancy routing. A perfect traffic split still breaks if the app exits in the middle of a request or reports healthy before it can reach the database.
- Add a real readiness check. Expose an endpoint that checks database connectivity, cache, and required dependencies within a short timeout. Return success only when the app can serve the user path. Do not reuse the liveness probe for this.
- Handle shutdown signals. Catch SIGTERM, stop accepting new requests, and let in-flight requests finish. Set a drain timeout shorter than the load balancer timeout so connections close cleanly.
- Run at least two instances. Never deploy a single instance behind a load balancer. Keep one old instance serving while the new one starts and passes readiness.
- Make database changes compatible. Add new columns and tables in a separate step, then deploy code that writes both old and new shapes if needed. Drop old structures only after the old version is gone and a later release confirms no reads remain.
- Version APIs and assets. Give new API fields defaults and keep old fields for at least one release. Add a content hash to static file names or bump a version query string so browsers do not mix old HTML with new CSS or JavaScript.
- Drain and move traffic in small batches. Remove one instance from the load balancer, wait for active connections to close, then stop it. Add the new instance only after it passes readiness. Repeat in small batches and pause if error rate or latency moves.
- Move long work to queues. Put emails, image processing, exports, and webhooks on a queue with idempotent workers. Deploy workers separately from web traffic.
- Test the full user path during deploy. Run a synthetic check for sign in and checkout against production every minute. Use a test account and clean up after.
How to check it worked
Watch from outside first. Open a browser in private mode and run your key user path while the deploy is in progress. Use curl or your synthetic monitor to check the public health endpoint and a page that reads from the database. A 200 from a static health route proves little.
Compare error rate, 95th percentile latency, CPU, memory, database connections, and queue lag against the baseline you recorded. Look for 5xx spikes on the new version and on the old version. Old version errors during a rolling deploy often mean shared state changed too early.
Check logs for shutdown messages, connection reset, timeout, deadlock, and migration lock waits. Confirm old instances leave the load balancer before they stop. For WebSockets or long polling, test that clients reconnect and do not lose messages. Run the check again after the last old instance is gone.
Mistakes to avoid
Using the same endpoint for liveness and readiness. Liveness should restart a stuck process. Readiness should remove an instance from traffic. If they are the same, a brief database blip can restart every instance at once.
Dropping a column in the same release that stops using it. Old instances still run for a few minutes and will crash on missing columns. The same applies to renaming a table, changing a cache key format, or changing a session cookie payload.
Sending traffic to a new instance before migrations finish, or running a long migration inside the deploy. Both create timeouts under load. Also avoid sticky sessions as a substitute for shared state. They hide the problem until a node dies or a user lands on a new instance.
Trusting DNS only. DNS caching and TTL delays mean some clients keep hitting old addresses. Use a load balancer or proxy you control for the cutover, and keep the old endpoint alive until traffic drains. If you want help building this, Cloudverg's Delivery platform setup is a fixed price $14,500 package that covers pipelines, environments, monitoring, and a tested rollback. You can also do it with your own scripts and a load balancer.
Frequently asked questions
Can you deploy without downtime on a single server?
Not safely for a normal app deploy. You need a second copy or a proxy that can hold requests while the process restarts. For small apps, a short maintenance window is more honest than a risky single-node swap.
Do I need Kubernetes for zero downtime deploys?
No. A load balancer with two virtual machines, health checks, and a graceful shutdown script can do it. Kubernetes gives you rolling updates and readiness probes, but you still have to configure database migrations and shutdown correctly.
How do I handle database migrations without downtime?
Use expand and contract. Add the new structure first, deploy code that reads and writes both shapes, backfill data in batches, then remove the old structure in a later release. Keep each step small and monitor lock waits.