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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. 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.
  2. 02Inspect deserialization and validation logs for unknown fields, unknown enum values, or unparseable payloads that the binary handled as default or skipped.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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