Filter concepts by levelShowing all levels.

Django · Deployment Strategies

Deployment Strategies

Two versions, one database — and a way back that actually exists

  • One variable: traffic share over timerolling ramps, blue/green steps, canary holds — the rest follows
  • Both versions share one schemaa canary that drops a column breaks the 95% that did not get the code
  • Fail readiness before you stop servingthat ordering is the whole of a zero-downtime drain
  • A flag undoes in one requesta rollback needs a pipeline run — and a schema that still fits
  • Write the rollback plan firstversion, trigger, and what going back will not fix

1.Rolling, blue/green and canary

A **rolling** deploy replaces instances a few at a time, so both versions serve traffic while it runs. **Blue/green** brings up a whole second fleet, tests it, then switches all traffic at once — and switches back just as fast. A **canary** sends a small share of real traffic to the new version, watches the error rate, and only then continues. All three run two versions of your code against **one** database.

Key features

  • All three run two code versions against one database — the schema must fit both
  • Rolling: no extra capacity, both versions live, rollback is another full roll
  • Blue/green: double capacity briefly, instant switch, instant switch back
  • Canary: real evidence before full exposure — but only with per-version metrics
  • Sessions, cache values and queued task payloads must be readable by both versions
  • Workers are a third fleet: old consumers will read messages the new code wrote

The three strategies, compared on what they actually cost

The three strategies, compared on what they actually cost
DimensionRollingBlue/greenCanary
extra capacitynone**2× during the switch**a little
both versions liveyes, throughoutbriefly, at the switchyes, by design
blast radius of a bad releasegrows as it rollseveryone, instantly**the canary share only**
rollbackanother full rollflip back — secondsstop and drain the canary
evidence before full exposurenonesmoke tests, no real trafficreal traffic, real users
needsa health checka second fleet and a switchtraffic splitting + per-version metrics

What must be compatible across the two live versions

What must be compatible across the two live versions
Shared thingThe failure if it is notThe discipline
database schemaold code queries a column the new release droppedexpand → migrate → switch → contract, one release later
session contentsrandom logouts, or a `KeyError` on a session keyadd keys; never repurpose or remove one in the same release
cache valuesa pickled object the other version cannot readversion the cache key when the shape changes
queued task payloadsold worker gets an argument it has no parameter foradd optional arguments only; never rename a task in place
static asset URLsa page from v2 requests an asset only v2 collectedhashed filenames, and keep the previous build served
Share of traffic on the new version, over the deploy

One variable separates the three: how fast the new version reaches 100% of traffic. Everything else — capacity cost, blast radius, speed of rollback — follows from that curve.

  • Three small step charts side by side, each plotting the share of traffic served by the new version against time, from zero per cent at the bottom to one hundred per cent at the top.
  • Rolling is a staircase climbing in four equal steps from zero to one hundred per cent, labelled "no extra capacity, both versions live throughout".
  • Blue/green is flat at zero and then jumps straight to one hundred per cent in a single step, labelled "double capacity briefly, and the fastest rollback".
  • Canary is flat at zero, rises to a narrow five per cent shelf that it holds for a long stretch, then jumps to one hundred per cent, labelled "evidence from real traffic before full exposure".
  • A footer states that in all three cases both versions share one database, one cache and one queue, so the schema in effect must satisfy the old code and the new code at the same time.

Command

bash
./deploy.sh --strategy canary --share 5 --watch-minutes 15

The three strategies differ only in how fast the new version reaches 100% of traffic: rolling ramps with no extra capacity, blue/green steps with double capacity and the fastest rollback, and a canary holds a small share while you gather real evidence. All of them run two versions of the code against one database, one cache and one queue, so the schema, the session format, the cache values and the task payloads must all satisfy both versions at once — which is expand-and-contract, not a strategy choice. And a canary only works with per-version metrics; without them, five per cent of the errors disappears into normal variation.

2.Feature flags: deploying is not releasing

A **feature flag** is a runtime switch around new behaviour. The code ships to production turned off, and you turn it on separately — for internal users first, then a percentage, then everyone. That splits one risky event into two: **deploying** the code, which is now boring, and **releasing** the behaviour, which is reversible in seconds without a deploy.

Key features

  • Deploy the code dark, then release the behaviour — two events, two risks
  • Turning a flag off takes effect at the next request; a rollback takes a pipeline run
  • Four kinds: release (temporary), operational (permanent), experiment, permission
  • A "permission flag" is authorisation — it belongs in the permission model, not here
  • Every flag doubles the state space, so it needs an owner and a removal date
  • Evaluate from cache, and define the value for when the flag store is unreachable

Four things people call a feature flag

Four things people call a feature flag
KindLifetimeOwned byBelongs in
release flagweeks — delete after 100%the team shipping the changea flag store
operational flagpermanentwhoever is on calla flag store, with a runbook entry
experiment flagthe length of the experimentwhoever reads the resultsan experiment tool
permission flagthe life of the accountthe product**your permission model**, not a flag store
One event, or two

Deploy = release

  • New code and new behaviour arrive in the same minute
  • A problem could be the build, the config or the feature — you cannot tell
  • Undo is a pipeline run: minutes, and only if the schema allows it
  • Every change waits for a deploy window, so changes get batched
  • Batched changes make the next incident harder to attribute

Deploy, then release

  • Code ships dark; the deploy proves only that nothing broke
  • Behaviour is enabled for staff, then 5%, then everyone
  • Undo is a flag write: effective on the next request
  • A bad feature does not force a rollback of unrelated changes
  • The cost is a branch in the code, which you must later delete
  • Deploy = release
    • New code and new behaviour arrive in the same minute
    • A problem could be the build, the config or the feature — you cannot tell
    • Undo is a pipeline run: minutes, and only if the schema allows it
    • Every change waits for a deploy window, so changes get batched
    • Batched changes make the next incident harder to attribute
  • Deploy, then release
    • Code ships dark; the deploy proves only that nothing broke
    • Behaviour is enabled for staff, then 5%, then everyone
    • Undo is a flag write: effective on the next request
    • A bad feature does not force a rollback of unrelated changes
    • The cost is a branch in the code, which you must later delete

Command

python
if flags.enabled("checkout.new-tax-engine", user=request.user):

A flag splits one risky event into two: the deploy, which becomes boring because the code ships dark, and the release, which you control and can reverse at the next request instead of at the next pipeline run. Know which kind you are adding — a release flag is temporary and must be deleted, an operational flag is a permanent degradation switch, and a "permission flag" is authorisation that belongs in your permission model. Evaluate from cache so a flag in a loop is not an N+1, decide what the value is when the store is unreachable, and bucket users by a stable hash so a percentage rollout does not flicker.

3.Draining, health checks and the smoke test that decides

A **graceful shutdown** stops accepting new requests, finishes the ones in flight, and only then exits. The **health check** is what the load balancer polls to decide whether to send you traffic — so failing it *first*, while still serving, is what makes the drain invisible. A **smoke test** is the handful of real requests run against the new deployment to decide whether the release stands or is rolled back.

Key features

  • Fail readiness first, keep serving, then stop accepting — that order is the drain
  • Wait at least one balancer check interval before shutting down, or you race it
  • Grace periods nest: gunicorn graceful-timeout < systemd/platform grace period
  • Readiness may check the database; liveness must not, or a blip restarts the fleet
  • Smoke tests are the first evidence the artefact works *in this environment*
  • A smoke failure that does not trigger a rollback is a notification, not a gate

The shutdown sequence, and what breaks if you skip a step

The shutdown sequence, and what breaks if you skip a step
StepWhat it doesSkip it and
fail readinesstells the balancer to stop routing herenew requests arrive at a draining process
keep servingcovers the balancer's next check intervalyou race the balancer and lose
SIGTERM to the masterbegins gunicorn's graceful shutdown—
finish in-flight requestsup to `--graceful-timeout`responses are cut off mid-write
exitthe process ends before the platform's deadlinethe platform sends SIGKILL instead

Three checks that get confused

Three checks that get confused
CheckAsksFailure means
livenessis this process wedged?restart the container
readinessshould this instance get traffic?remove it from the pool
smoke testdoes this release actually work here?roll the release back
Replacing one instance without dropping a request
  1. t+0s

    Drain flag set; readiness starts failing

    the process is still serving normally — this is an announcement, not a state change

  2. t+0s → t+10s

    The balancer notices

    one check interval, plus a margin. New requests stop arriving; in-flight ones continue

  3. t+10s

    SIGTERM to the gunicorn master

    gunicorn: "graceful shutdown; waits for workers to finish requests up to graceful_timeout"

  4. t+10s → t+40s

    Draining

    no new connections are accepted; typically finishes in a second or two because nothing new is arriving

  5. t+40s

    Force kill of anything still running

    gunicorn kills stragglers at the graceful timeout — the window is a guarantee, not a plan

  6. t+55s

    Platform grace period would fire

    deliberately later than every step above; if it fired first, it would kill a healthy drain

  7. after

    New instance starts, readiness passes, smoke test runs

    the balancer adds it only once readiness passes; the smoke result decides whether the release stands

  1. t+0s: Drain flag set; readiness starts failing — the process is still serving normally — this is an announcement, not a state change
  2. t+0s → t+10s: The balancer notices — one check interval, plus a margin. New requests stop arriving; in-flight ones continue
  3. t+10s: SIGTERM to the gunicorn master — gunicorn: "graceful shutdown; waits for workers to finish requests up to graceful_timeout"
  4. t+10s → t+40s: Draining — no new connections are accepted; typically finishes in a second or two because nothing new is arriving
  5. t+40s: Force kill of anything still running — gunicorn kills stragglers at the graceful timeout — the window is a guarantee, not a plan
  6. t+55s: Platform grace period would fire — deliberately later than every step above; if it fired first, it would kill a healthy drain
  7. after: New instance starts, readiness passes, smoke test runs — the balancer adds it only once readiness passes; the smoke result decides whether the release stands

Command

bash
POST /internal/drain   ->   readiness 503, still serving   ->   SIGTERM

Announce unavailability before becoming unavailable: fail readiness, keep serving for at least one balancer check interval, then `SIGTERM`, then drain, then exit — and make sure the platform's grace period is longer than the sum, or it sends the kill instead. Readiness may check dependencies and should be cached; liveness must not touch the database, or a blip restarts the fleet. Then run a smoke test against the deployed environment, because that is the first evidence this artefact works with real configuration, real credentials and real static files — assert on content rather than status, and wire the failure to the rollback, or it is a notification rather than a gate.

4.Rollback planning, and why migrations make it hard

A rollback plan is a sentence written **before** the deploy: which version we go back to, what makes us decide, and whether going back actually works. It often does not, because a deploy changes two things with different reversibility — the code, which is a redeploy away, and the database, which is not. Some changes have no rollback at all, and the time to discover that is while planning, not during an incident.

Key features

  • A rollback plan names the version, the trigger and whether going back works
  • Additive schema → rollback is a redeploy. Destructive schema → forward fix only
  • Reversing a migration restores the shape, not the values — a data transform is one-way
  • Emitted side effects (email, payment, webhook) need compensation, not rollback
  • Write the point at which rollback stops being possible into the release notes
  • A feature flag is the reversible alternative when the schema cannot be

Four release shapes, and what "roll back" means for each

Four release shapes, and what "roll back" means for each
Release containsRollbackPlan
code onlyredeploy the previous digestthe ordinary case — seconds, no schema involved
additive migrationredeploy; leave the new columnprevious version never mentions it, so it is inert
destructive migration**none**forward fix only — say so before deploying
data transform / deletion**none** (shape can return, values cannot)take a verified backup; plan a restore path
side effects emittedcompensating action, not a rollbacka refund, a correction, a tombstone event

Making an unrollbackable change rollbackable

Making an unrollbackable change rollbackable
Instead ofDoBecause
drop a column in the releasedrop it two releases laterthe previous version stops needing it first
rename a columnadd, dual-write, switch reads, drop latera rename is a drop and an add at the same instant
add `NOT NULL` to an existing columnadd nullable, backfill, then constrainthe constraint fails on rows the old code still writes
change behaviour in the deployship dark behind a flaga flag is reversible at the next request
rewrite rows in a migrationbackfill in a resumable commanda migration that fails halfway leaves neither shape
What kind of undo a release actually has
Redeploy the previous digest
The ordinary release. The old image still runs against this schema, so rollback is one command and takes seconds.
Roll back, then compensate
Code goes back cleanly, but 400 emails were sent and 12 payments captured. The undo is a product decision: refunds, corrections, a tombstone event.
Forward fix only
A dropped or renamed column means the previous version cannot start. Say this in the release notes before deploying, not during the incident.
Restore, not rollback
Destroyed data plus emitted effects. This needs a verified backup, a restore rehearsal and a communications plan — it is an incident procedure, not a deploy step.
  • Redeploy the previous digest: schema: additive only, no side effects emitted — The ordinary release. The old image still runs against this schema, so rollback is one command and takes seconds.
  • Roll back, then compensate: schema: additive only, side effects already emitted — Code goes back cleanly, but 400 emails were sent and 12 payments captured. The undo is a product decision: refunds, corrections, a tombstone event.
  • Forward fix only: schema: destructive, no side effects emitted — A dropped or renamed column means the previous version cannot start. Say this in the release notes before deploying, not during the incident.
  • Restore, not rollback: schema: destructive, side effects already emitted — Destroyed data plus emitted effects. This needs a verified backup, a restore rehearsal and a communications plan — it is an incident procedure, not a deploy step.

Command

text
Rollback: redeploy <digest> · Trigger: <observation> · Valid until: <release>

Write the rollback plan with the change, not after it: the version and digest, the command, the trigger that decides, and — most importantly — what going back will *not* fix. A deploy changes two things with different reversibility, so classify it: additive schema means a redeploy is enough, destructive schema means forward fix only, a data transform cannot be undone by reversing a migration because the shape returns without the values, and emitted side effects need compensating actions rather than a rollback. Name the release where the plan expires. And when a change has no rollback, use the two tools that give you one: a feature flag, and expand-and-contract.

Remember

  • A canary without per-version metrics is a slow rolling deploy: 5% of the errors disappears into normal variation.
  • Sessions, cache values and queued task payloads are shared between the two live versions too — not just the schema.
  • Wait at least one load-balancer check interval after failing readiness, or the drain races the balancer and loses.
  • Reversing a migration restores the column, not the values. A data transform has no undo.

Pro tip

  • Give every release plan a "valid until" line naming the release that makes rollback impossible. It turns a vague reassurance into a date, and it makes the next release's risk visible while it is still cheap to change.
  • Smoke tests should assert content, not status. A Django error page, a maintenance banner and a login screen all return 200.

Next up

  • Safe Django Upgrades
  • Architecture Styles
  • Service Layer

Sources

  • Django migrations topic guide — docs.djangoproject.com/en/5.2/topics/migrations/
  • Django cache API — docs.djangoproject.com/en/5.2/topics/cache/
  • Gunicorn signal handling and settings — gunicorn.org
  • Every Django claim checked against Django 5.2 LTS docs · checked 2026-09-05

Spotted a mistake on this page? Tell us.