Deployment · advanced
How to verify a deployment rollback with compatible state
Engineers face a subtle verification problem after a deployment rollback: the previous release is now serving traffic, but it must be proven to interoperate with the current schema, configuration, and downstream contracts. This guide frames rollback verification as a compatibility check between the rolled-back binary and the live state, not as a restart confirmation. It covers observable signals, decision boundaries, and proof criteria for a rollback that is correct on paper but unsafe in practice.
The symptoms
- •Traffic has shifted to the previous version, yet error rates, 5xx totals, or specific endpoint failure ratios remain elevated compared to the pre-incident baseline.
- •Distributed traces show the rolled-back version completing requests but failing downstream calls, retries, or schema validations against shared stores.
- •Schema-aware features (deserializers, migrations, validators) log warnings, fallback paths, or coercion messages that were absent before the original incident.
- •Configuration reads succeed, but values returned for feature flags, secrets, or environment keys differ from the labels the rolled-back binary expects.
- •Health endpoints report ready, but synthetic monitors that read or write through the application surface partial or incompatible responses.
Likely causes
- •Forward-only schema changes (column additions, type widening, new indexes) executed between the original release and the rollback target, leaving the older binary unable to read new fields cleanly.
- •Configuration or feature-flag schema drift that the rolled-back version cannot parse, so it silently consumes defaults or rejects the payload.
- •Downstream contract changes (API fields, message broker payload schemas, queue topics) performed after the rolled-back version was last exercised in production.
- •State machine divergence: rows, documents, or events written by the newer version carry statuses, enums, or transitions the older code path does not recognize.
- •Side-effects persisted by the newer version (background jobs, cache entries, queued messages) that the rolled-back version cannot consume or finish correctly.
First ten minutes
- 01Confirm the rollback target is actually serving: check the deployment controller's current revision, the active replica set, and the version label on the live pods versus the rolled-back label.
- 02Capture the exact version, build identifier, or commit hash of the running binary and compare it against the intended rollback target in the release manifest.
- 03Inventory the stateful dependencies the binary reads and writes: database, cache, object store, message broker, and feature-flag service. Record their current schema versions and configuration revision.
- 04Pull a small, recent sample of error logs and traces from the rolled-back version and tag each failure with the dependency it last touched successfully.
- 05Decide whether the failure pattern is concentrated on a dependency boundary (storage, broker, config) or distributed across the request path, because the verification scope differs.
- 06Stop here if the running version does not match the rollback target; the rest of the guide assumes the correct binary is serving.
Evidence to collect
- •Active version identifier on every replica, with timestamp of last observed shift, cross-referenced against the rollback manifest.
- •Schema fingerprint for each stateful dependency: column lists, document field sets, topic schemas, and index definitions observed at the moment of verification.
- •Configuration revision identifier and the resolved values for feature flags, secrets, and environment keys the rolled-back binary reads on startup and per request.
- •Sample of recent writes produced by the newer version that are now being read by the rolled-back version, including any unrecognized fields or enum values.
- •Error and trace samples filtered to the rolled-back version label, grouped by the first dependency the request reached.
Where to look
- •The boundary between the rolled-back process and the database or object store: connection pool, migration runner, ORM or driver logs, and query error counters.
- •The boundary between the rolled-back process and the message broker: consumer group offsets, schema registry versions, and dead-letter or poison-message counts.
- •The boundary between the rolled-back process and the configuration or feature-flag service: fetched values, schema of the response, and diff against the binary's expected keys.
- •The boundary between the rolled-back process and downstream HTTP, gRPC, or queue APIs: contract versions, accepted content types, and field-level validation errors.
- •Persistent state produced between the original release and the rollback: new table rows, new event types, new cache keys, and new background job records.
Diagnostic steps
- 01Compare the dependency schema fingerprint against the maximum schema the rolled-back binary was ever tested against in the release history; mismatch indicates forward-only drift.
- 02Inspect deserialization and validation logs for unknown fields, unknown enum values, or unparseable payloads that the binary handled as default or skipped.
- 03Replay a small, read-only sample of records written by the newer version through the rolled-back binary's read path, in an isolated environment, and observe the resulting object state.
- 04For each downstream contract, verify the rolled-back binary's expected request shape and the live contract's advertised shape against the schema registry or contract documentation.
- 05Check configuration: fetch the resolved configuration the rolled-back binary is using at startup and compare keys, types, and acceptable values against the binary's documented expectations.
- 06Trace a representative end-to-end request and identify the first dependency boundary where the rolled-back binary's behavior diverges from the newer version's, then focus evidence collection there.
- 07Decide: if divergence is confined to a single boundary and the binary can read, write, and reject consistently, the rollback is compatible; if divergence touches multiple boundaries or produces silent coercion, the rollback is not safe to hold.
Common mistakes
- •Treating a successful rollout status as proof of compatibility, when rollout status only confirms traffic shifted, not that the binary can read the current state.
- •Verifying only the request path while ignoring consumers, workers, or schedulers that read state written by the newer version.
- •Assuming forward-compatible schema changes are automatically backward-compatible, especially for enum additions, type changes, and required-field additions.
- •Comparing the rolled-back binary's behavior against current documentation rather than against the schema and configuration actually present in the environment.
- •Stopping verification at the first dependency boundary where the rolled-back binary appears to work, without checking write paths and background processing.
- •Using cached configuration or warmed-up pools during verification, which can mask first-request failures in the rolled-back binary.
Safe fixes
- •Conditional on a confirmed schema mismatch: halt the rollback hold, drain traffic back to the version that wrote the current state, and only re-attempt the rollback after the schema is rolled forward or the data is back-filled to a shape the rolled-back binary can read.
- •Conditional on a configuration drift: pin the rolled-back binary to the configuration revision it was last validated against, and document the configuration delta as a separate change requiring its own verification.
- •Conditional on downstream contract mismatch: keep the rolled-back binary on a read-only or degraded path until the downstream contract is restored to a version both binaries accept, and tag affected requests for manual review.
- •Conditional on silent coercion (unknown fields ignored, defaults substituted): do not rely on silent behavior; require an explicit compatibility statement from the binary's schema or contract tests before declaring the rollback safe.
- •Conditional on background workers or consumers being unable to process newer-version state: pause those workers, replay their inputs through the rolled-back binary in an isolated harness, and only resume them after the replay succeeds.
Prove the fix
- 01Synthetic and canary traffic through the rolled-back binary completes end-to-end without producing unknown-field, unknown-enum, or unparseable-payload warnings, and the error rate returns to the pre-incident baseline within an agreed tolerance.
- 02Read-only replays of records written by the newer version, executed through the rolled-back binary's read path, produce the same logical objects as the newer version's read path, with no silent default substitution.
- 03Configuration diff between the rolled-back binary's last known-good revision and the live revision is empty, or every non-empty entry is explicitly covered by the binary's compatibility tests.
- 04Background workers and consumers drained their queues without dead-lettering, and the counts of unrecognized payloads, failed validations, and fallback paths are zero over the verification window.
- 05Two independent verification runs, separated by a configuration or schema refresh, both report compatible state; a single passing run is insufficient evidence when dependencies are versioned.
Prevention and next steps
- •Maintain a machine-readable compatibility matrix that records, for each released version, the maximum schema, configuration, and downstream contract it has been tested against.
- •Require every forward-only change to ship with an explicit backward-compatibility statement and a regression test that exercises the previous version against the new state.
- •Treat rollback as a first-class deployment path: rehearse it in pre-production, with the same schema and configuration the production environment will hold, and capture the verification artifacts.
- •Separate rollback verification from restart verification: a script that proves the binary is running is not a script that proves the binary is compatible with current state.
- •Capture the verification evidence in the incident record so the next rollback can diff against a known-good compatibility baseline rather than re-deriving it under time pressure.
Safe commands and checks
kubectl get deploy -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.generation}{"\t"}{.spec.template.metadata.labels.version}{"\n"}{end}'
kubectl get pods -l app=<app-name> -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].image}{"\n"}{end}'
kubectl rollout history deploy/<deployment-name>
kubectl logs -l app=<app-name>,version=<rollback-version> --since=10m --tail=200 | grep -Ei 'unknown|unparseable|fallback|default|schema|enum'
psql -h <db-host> -U <db-user> -d <db-name> -c "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '<table-name>' ORDER BY ordinal_position;"
redis-cli -h <redis-host> INFO keyspace
jq '.resolvers[] | {name, current, target}' schema-registry/compat.json