{
  "slug": "production-deploy",
  "url": "https://checklists.org/production-deploy",
  "markdown": "https://checklists.org/production-deploy.md",
  "title": "Deploying to Production",
  "description": "Everything to check before, during, and after shipping a change to a production web service.",
  "category": "software",
  "tags": [
    "software",
    "operations"
  ],
  "version": "1.0",
  "updated": "2026-07-07",
  "sources": [
    {
      "name": "Google SRE Book — Release Engineering",
      "url": "https://sre.google/sre-book/release-engineering/"
    },
    {
      "name": "Martin Fowler — Feature Toggles (Pete Hodgson)",
      "url": "https://martinfowler.com/articles/feature-toggles.html"
    }
  ],
  "intro": "For shipping a change to a production web service with real users. Assumes you have CI, at least one pre-production environment, and dashboards for errors and latency.",
  "itemCount": 23,
  "essentialCount": 6,
  "sections": [
    {
      "title": "Before you deploy",
      "items": [
        {
          "id": "9bb7vx",
          "text": "Confirm CI is green on the exact commit you are deploying",
          "note": "The commit, not the branch. A merge landed after your last CI run can ship code no test has ever seen.",
          "essential": true
        },
        {
          "id": "v2879w",
          "text": "Skim the full diff between production and the release candidate",
          "note": "What's going out is often more than your change — everything merged since the last deploy rides along."
        },
        {
          "id": "18slmt7",
          "text": "Write down the rollback plan",
          "note": "The exact command, the version identifier you'd roll back to, who runs it, and how long it takes. If the plan is \"redeploy the old image\", verify that image still exists in the registry.",
          "essential": true
        },
        {
          "id": "2frn1x",
          "text": "Verify the migration works with the currently running code",
          "note": "Old code and new schema run side by side during rollout. Use expand/contract: add columns as nullable now, backfill, and drop the old ones in a later deploy — never in the same one.",
          "when": "database-migration",
          "essential": true
        },
        {
          "id": "lrh4yh",
          "text": "Rehearse the migration against a production-sized dataset",
          "note": "An `ALTER TABLE` that is instant in staging can lock a 100-million-row table for minutes. Measure it before production does.",
          "when": "database-migration",
          "essential": true
        },
        {
          "id": "14pvxkc",
          "text": "Put the new behavior behind a feature flag, deployed off",
          "note": "Separates deploy from release. Flipping a flag back takes seconds; even a good rollback takes minutes.",
          "when": "risky-change"
        },
        {
          "id": "1uhf7jp",
          "text": "Pick a deploy window when people can respond",
          "note": "Not Friday afternoon, not ten minutes before you leave, not peak traffic. The best window is early in a workday with the author and on-call both around."
        },
        {
          "id": "j3lzop",
          "text": "Announce the deploy in the team channel",
          "note": "What's going out, a link to the diff, and who is driving. This is also what turns \"is anyone deploying?\" from a mystery into a scroll."
        },
        {
          "id": "1klcoed",
          "text": "Post a maintenance window to the status page",
          "note": "Customers forgive planned downtime and punish surprise downtime.",
          "when": "expected-downtime"
        }
      ]
    },
    {
      "title": "During the deploy",
      "items": [
        {
          "id": "5da16e",
          "text": "Run the migration before rolling out the new code",
          "note": "New code that expects a schema that isn't there yet fails loudly and everywhere at once.",
          "when": "database-migration",
          "essential": true
        },
        {
          "id": "iyv2el",
          "text": "Roll out to a canary slice first",
          "note": "5–10% of traffic, then compare error rate and latency against the rest of the fleet before going wide. Most bad deploys announce themselves within minutes at 5%."
        },
        {
          "id": "1yosod4",
          "text": "Watch error and latency dashboards while instances cycle",
          "note": "Have them open *before* the rollout starts so you can see the before/after edge, not just the after."
        },
        {
          "id": "1abxleb",
          "text": "Verify the new version is actually serving",
          "note": "Hit the version or build-hash endpoint. \"Deploy succeeded\" sometimes means the orchestrator gave up and left old containers running."
        },
        {
          "id": "1l74c7f",
          "text": "Smoke-test the critical path by hand",
          "note": "Log in, perform the core action users pay for, log out. Two minutes, catches the class of failure health checks can't see."
        },
        {
          "id": "1uyy5nl",
          "text": "Roll back at the first sign of user-facing errors",
          "note": "Roll back first, diagnose later. Debugging forward in production turns a five-minute incident into an hour-long one — the rollback plan you wrote costs nothing to use.",
          "essential": true
        }
      ]
    },
    {
      "title": "First hour after",
      "items": [
        {
          "id": "11ey86t",
          "text": "Enable the flag for an internal or small cohort first",
          "note": "Employees, then 1–5% of users, then everyone. Each step is a chance to catch what staging couldn't.",
          "when": "risky-change"
        },
        {
          "id": "1s7a762",
          "text": "Keep watching for at least 30 minutes",
          "note": "Some failures need a cache expiry, a cron run, or a traffic peak to surface. A deploy isn't done when the progress bar finishes."
        },
        {
          "id": "119ckla",
          "text": "Compare key business metrics against the same window yesterday",
          "note": "Errors can be silent: a signup flow that drops 30% of users throws no exceptions."
        },
        {
          "id": "z2dg4f",
          "text": "Check background jobs and queue depths",
          "note": "A worker crashing on the new code shows up as a growing queue, not an alert — a slow-motion outage."
        },
        {
          "id": "4gfxdj",
          "text": "Announce completion in the same channel",
          "note": "Include anything you noticed; the next deployer inherits your context."
        }
      ]
    },
    {
      "title": "Follow-up",
      "items": [
        {
          "id": "ai4gd9",
          "text": "Remove the feature flag once the change is fully rolled out",
          "note": "A stale flag is an untested code path and a config landmine. Put a removal ticket in the sprint the day you flip it to 100%.",
          "when": "risky-change"
        },
        {
          "id": "f7bd28",
          "text": "Schedule the contract step of the migration",
          "note": "Drop the old columns and code paths after a safe soak period, or they'll still be there in five years.",
          "when": "database-migration"
        },
        {
          "id": "1wztjpx",
          "text": "Update the runbook with anything this deploy taught you",
          "note": "If a step surprised you, it will surprise the next person at 3 a.m."
        }
      ]
    }
  ]
}