Part of Hosting by frameworkHosting · Deploys

Zero-downtime deploys with health checks, and what Canner does today.

A zero-downtime deploy starts the new version beside the old one, sends it traffic only after a health check passes, then sends the old one SIGTERM so it can finish its requests. Canner health-checks every deploy and gives the old process 30 seconds, but it stops the old version before starting the new one, so expect a short gap.

Facts checked

What a /health endpoint should return

Status codes, not bodies. Every platform’s checker reads the HTTP status. A 200 means “send me traffic”, a 5xx means “not yet”. The JSON body is for you, when you curl it.

Kubernetes names the two questions well. A readiness probe asks whether the app can take traffic now; a liveness probe asks whether it is stuck and should be restarted. A deploy health check is a readiness check: it should fail when the app can’t do its job, such as when it can’t reach its database, and pass otherwise.

  • Fast. Answer in well under a second. Canner waits up to 3 seconds per attempt.
  • No auth, no redirect. Exempt the path from login, HTTPS redirects and rate limits.
  • Check what the app can’t live without. select 1 against the database catches a wrong DATABASE_URL at deploy time instead of at the first visitor.
  • Don’t check what you can live without. If a third-party API is down, failing your health check takes your whole app down with it.
// Express. Fast, unauthenticated, no redirects.
app.get("/healthz", async (req, res) => {
  try {
    await pool.query("select 1");          // can we reach the database?
    res.status(200).json({ ok: true });
  } catch {
    res.status(503).json({ ok: false });   // 5xx = not ready
  }
});

How Canner’s health check reads your answer

These details come from the platform code, and they change what your endpoint should do.

  • It polls the health-check path (/ unless you set another in project settings) every half second for up to 30 seconds after the new process starts.
  • Any status below 500 passes. A 200, a 302 to a login page and a 404 all mean “the server is answering”. A typo in the path still passes, so your database check only counts if the path really exists.
  • A 5xx keeps it retrying until the 30 seconds are up, then the deploy fails with the last status in the log. That is how a 503 from the endpoint above stops a deploy that can’t reach its database.
  • It sends X-Forwarded-Proto: https and never follows redirects, so an app that forces HTTPS (Rails 7.1+, Django SECURE_SSL_REDIRECT) isn’t mistaken for a broken one.
  • After it passes, Canner watches the process for another 10 seconds. An app that answers once and then crashes, a missing secret read lazily for instance, fails the deploy instead of going live and returning 502s.
  • Projects deployed as a worker skip the HTTP check; they only have to stay up.

What happens during a Canner deploy, in order

Canner does not yet run the old and new versions side by side; blue/green deployment slots, which will, are on our roadmap. A production deploy today goes like this:

  • The build runs in its sandbox. Your site is untouched.
  • The release command runs, if you set one, while the old version is still serving. If it fails, the deploy stops there and nothing changes.
  • The old process gets SIGTERM and up to 30 seconds to finish its requests. After that, SIGKILL.
  • The new process starts on the same port and is health-checked, then watched for 10 seconds.
  • If it passes, the deploy is live. If it fails, Canner stops it and, with auto-rollback on, rebuilds your last good commit and deploys that.

How other platforms swap versions

The difference that matters is whether the old and new versions overlap. Everything else is tuning.

PlatformOld and new overlap?Stop signalTime before forced kill
CannerNo: old stopped, then new startedSIGTERM30 s
Render †Yes: the original instance keeps all traffic while the new one startsSIGTERM30 s by default, up to 300 s
Heroku (default) †No: preboot is off unless you enable itSIGTERM30 s
Heroku with preboot †Yes, up to about 3 minutes; opt-in, Standard and Performance dynosSIGTERM30 s
Fly.io †Rolling by default, one Machine at a time; bluegreen needs at least one health checkSIGINT by default5 s by default, up to 300 s

† Competitor details come from each vendor’s own published pages on the dates shown; prices and plans change, so check the vendor before you decide. Render (2026-10-29) · Heroku (default) (2026-10-29) · Heroku with preboot (2026-10-29) · Fly.io (2026-10-29)

Graceful shutdown on SIGTERM

SIGTERM is the polite request. On receiving it, stop accepting new connections, finish what is in flight, close database connections and exit. Do it faster than the grace period, because the next signal can’t be caught.

Next.js’s next start installs its own SIGTERM handler. A plain Node server has none: without one, Node exits at once and cuts off open requests. Python’s gunicorn and Ruby’s Puma handle it in their own way; check that your start command runs the server directly (exec) and not behind a shell that swallows the signal.

const server = app.listen(process.env.PORT);

process.on("SIGTERM", () => {
  // Stop accepting connections, let in-flight requests finish.
  server.close(async () => {
    await pool.end();                       // close database connections
    process.exit(0);
  });
  // Leave before the platform's SIGKILL (30 s on Canner).
  setTimeout(() => process.exit(1), 25_000).unref();
});

Database changes that break the old code

Even with perfect overlap, a deploy is not zero-downtime if the migration breaks the version still running. On Canner the release command runs while the old version is serving; on platforms with overlap, old and new run together against one schema. Either way, old code meets the new schema.

Renaming or dropping a column the old code reads makes its requests fail until the new version takes over. Split those changes over two deploys: add first, remove later. Where migrations should run covers the steps, and why auto-rollback should be off for a deploy whose migration the previous commit can’t handle.

In-memory state is the other trap. Sessions, rate-limit counters and caches held in process memory are gone after every deploy. Keep sessions in signed cookies or your Postgres database.

Questions

What should a health check endpoint return?

A 200 when the app can serve requests and a 503 when it can’t, decided in well under a second, with no login or redirect in front of it. Check the database it depends on; don’t check optional third-party APIs.

What is the difference between liveness and readiness?

Readiness asks whether the app should receive traffic now; a failing one takes it out of rotation. Liveness asks whether it is stuck; a failing one restarts it. A deploy health check is a readiness check.

Are deploys on Canner zero-downtime?

Not yet. Canner stops the old process (SIGTERM, up to 30 seconds) before starting the new one, so requests during the switch get a 502. It health-checks the new version and rolls back if it fails. Blue/green slots that overlap the two versions are on the roadmap.

Why does my deploy pass the health check but the site shows errors?

On Canner any status below 500 passes, so a 404 or a login redirect counts as healthy. Point the health-check path at an endpoint that really checks the database, and look at Logs for the errors behind the page.

How long does an app get to shut down?

30 seconds on Canner, Heroku and Render’s default, and in Kubernetes by default. Fly.io defaults to 5 seconds. After that the process is killed.

Deploy with a health check that means something.

Every Canner deploy is health-checked, watched for crashes and rolled back if it fails. Setup for each framework is in hosting by framework.