DevOps ·

Production Readiness Reviews: A Pre-Launch Checklist for Startup Engineering Teams

A practical guide to production readiness reviews for startup engineering teams. Covers reliability, observability, security, data, deployment, operational readiness, and dependencies, with a concrete checklist and guidance on when to skip vs when to treat items as non-negotiable.

Production Readiness Reviews: A Pre-Launch Checklist for Startup Engineering Teams

You are two days from launching. The feature works in staging. The demo went well. Everyone is excited.

Then production falls over at 11pm on a Friday because you forgot to configure graceful shutdown, your logs have no request IDs, and your only “runbook” is a Notion page that was last edited six months ago.

Production readiness reviews exist to catch this before it costs you. Not as a bureaucratic gate, but as a forcing function: a structured moment where you ask “if this breaks at 2am, can we diagnose and recover quickly?” The answer tells you whether you are ready.

This guide covers what a production readiness review looks like in practice for a startup team, how to structure the checklist across seven categories, which items are non-negotiable regardless of team size, and how to scale the process from two engineers to twenty.


What a PRR Actually Is

A production readiness review is a pre-launch checklist executed by the team that built the system, reviewed by at least one engineer who did not build it. The goal is not to find bugs. It is to verify that the system can fail gracefully, can be observed when it does, and can be recovered by an engineer who is not the original author.

At a two-person startup, this takes about an hour. At a twenty-person org, it becomes a structured document with sign-offs. The items do not change much between those two scales. What changes is formality.


Category 1: Reliability

Reliability work answers the question: when something goes wrong, does the system degrade gracefully or does it fall over hard?

Health checks. Every service needs a liveness endpoint and a readiness endpoint. They are not the same thing. Liveness tells the orchestrator the process is alive. Readiness tells it the process is ready to serve traffic. A service that has started but has not finished loading its configuration or connecting to its database should fail readiness, not liveness.

// Express example: split health check endpoints
app.get("/healthz/live", (_req, res) => {
  // Process is running. No dependency checks here.
  res.status(200).json({ status: "ok" });
});

app.get("/healthz/ready", async (_req, res) => {
  try {
    // Check actual dependencies: DB, cache, etc.
    await db.query("SELECT 1");
    res.status(200).json({ status: "ready" });
  } catch (err) {
    res.status(503).json({ status: "not ready", reason: "db unreachable" });
  }
});

Graceful shutdown. When a SIGTERM arrives (during a deployment, a scale-down, or a pod eviction), your process should stop accepting new requests, finish in-flight requests, close database connections cleanly, and then exit. Without this, you drop requests on every deploy.

const server = app.listen(PORT, () => {
  console.log(`Listening on ${PORT}`);
});

process.on("SIGTERM", () => {
  server.close(() => {
    // Close DB pool, flush logs, etc.
    pool.end(() => process.exit(0));
  });

  // Force exit after 30 seconds if something is stuck
  setTimeout(() => process.exit(1), 30_000);
});

Retry logic with backoff. Outbound calls to databases, queues, and third-party APIs should retry on transient failures. Hard rule: use exponential backoff with jitter. A flat 1-second retry interval turns a brief hiccup into a thundering herd that makes the outage worse.

async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3,
  baseDelayMs = 200
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      const jitter = Math.random() * baseDelayMs;
      const delay = baseDelayMs * Math.pow(2, attempt - 1) + jitter;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}

Checklist items for reliability:

  • Liveness and readiness endpoints are separate and accurate
  • SIGTERM handler drains in-flight requests before exiting
  • All outbound calls retry with exponential backoff and jitter
  • Timeouts are set on every outbound HTTP call (no default “wait forever”)
  • Rate limits are handled (429 responses trigger backoff, not a crash)

Category 2: Observability

If you cannot see what your service is doing, you cannot debug it when it breaks. Observability is not optional for production systems.

Structured logging. Every log line should be JSON with a consistent schema: timestamp, level, service name, request ID, and the message. The request ID is the most important field. Without it, you cannot correlate a user’s error report to the specific log lines that explain what happened.

// Attach a request ID at the edge of your system
import { randomUUID } from "crypto";

app.use((req, _res, next) => {
  req.requestId = req.headers["x-request-id"] as string ?? randomUUID();
  next();
});

// Use a structured logger that carries context
const logger = {
  info: (msg: string, ctx?: Record<string, unknown>) =>
    console.log(JSON.stringify({ level: "info", msg, ...ctx, ts: Date.now() })),
  error: (msg: string, ctx?: Record<string, unknown>) =>
    console.log(JSON.stringify({ level: "error", msg, ...ctx, ts: Date.now() })),
};

// Usage in a route handler
app.get("/orders/:id", async (req, res) => {
  const log = (msg: string, ctx?: object) =>
    logger.info(msg, { requestId: req.requestId, orderId: req.params.id, ...ctx });

  log("fetch order: start");
  const order = await getOrder(req.params.id);
  log("fetch order: done", { status: order.status });
  res.json(order);
});

Metrics. You need at minimum: request rate, error rate, and latency (p50, p95, p99). These three together tell you whether something is wrong. If you are on Cloudflare, this is mostly free. If you are running Node on a VPS, Prometheus with a simple histogram is a one-hour setup that pays for itself in the first incident.

Alerting. Alerts should be based on symptoms, not causes. Alert on error rate above 1% for 5 minutes, not “CPU above 80%.” CPU spikes are noise; user-visible errors are signal. Every alert that fires must either trigger an action or be deleted. Alert fatigue is a silent killer of on-call culture.

Tracing. For distributed systems (multiple services, queues, async workers), distributed traces let you follow a request across service boundaries. OpenTelemetry is the standard. You do not need a trace on every request at launch, but you do need trace context propagated so you can add sampling later without re-instrumenting.

Checklist items for observability:

  • All log lines are structured JSON with timestamp, level, service, and request ID
  • Request rate, error rate, and latency (p95) are instrumented
  • At least one alert fires on user-visible error rate, tested in staging
  • Alerts have a runbook link or description in the alert body
  • Trace context (W3C traceparent header) is propagated across service calls

Category 3: Security

Security items are the category most often skipped on tight timelines. Some of them are genuinely optional at launch. Others are non-negotiable.

Non-negotiable before any production traffic:

  • Authentication and authorization are working correctly. Test that an unauthenticated request returns 401, and that a user cannot access another user’s data.
  • Secrets are not in the codebase, environment variable files, or build artifacts. They live in a secrets manager (AWS Secrets Manager, Doppler, Infisical). Run git log -p | grep -i "secret\|password\|token\|api_key" before launch.
  • Input validation exists at the API boundary. Treat all inbound data as untrusted. Zod or a similar schema validator at the route level is the minimum.

CORS configuration. If your API is called from a browser, CORS must be explicitly configured. Wildcard origins (*) are fine for public read-only APIs. For anything with authentication, CORS must restrict to your known origins.

import cors from "cors";

const allowedOrigins = [
  "https://app.yourproduct.com",
  process.env.NODE_ENV === "development" ? "http://localhost:3000" : "",
].filter(Boolean);

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error(`CORS: origin not allowed: ${origin}`));
      }
    },
    credentials: true,
  })
);

Security headers. A one-line Helmet.js integration covers the basics: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options. These take five minutes and protect against an entire class of browser-based attacks.

Checklist items for security:

  • Auth is tested: unauthenticated requests are rejected, cross-tenant access is blocked
  • No secrets in source code or build artifacts (run a grep before launch)
  • Input validation on all API endpoints that accept user data
  • CORS is explicitly configured, not left as wildcard for authenticated routes
  • Security headers are set (Helmet.js or equivalent)
  • Dependencies are scanned for known CVEs (npm audit or Snyk in CI)

Category 4: Data

Data failures are the category where mistakes are hardest to undo.

Backups. If you are on a managed database (RDS, PlanetScale, Supabase), automated backups are probably configured by default. Verify this before launch, not after. Also verify you have tested a restore. A backup you have never restored is a backup you cannot trust.

Migrations. Database schema migrations must be backward compatible at launch. Zero-downtime deployments require that the new code can run against the old schema and the old code can run against the new schema simultaneously, for at least one deploy cycle. Adding a nullable column: safe. Renaming a column: not safe without a multi-step migration.

Retention policies. Every table that accumulates data should have a retention policy decided before you put real data in it. “We will figure it out later” means you will have a compliance problem, a query performance problem, and a storage cost problem at the same time, six months from now.

Checklist items for data:

  • Automated backups are enabled and the last backup time is verified
  • A restore has been tested in the last 30 days (or before first launch)
  • Migrations are backward compatible and have been tested against a copy of production data
  • Sensitive data fields are identified and access is logged
  • Data retention policies exist for high-volume tables

Category 5: Deployment

Rollback plan. Every deployment should have a defined rollback procedure. For most teams this is: deploy previous container image, run down-migrations if schema changed. The rollback must be practiced. If you have never rolled back, your rollback plan is theoretical.

Feature flags. For significant new features, shipping behind a feature flag lets you decouple deployment from release. You can deploy the code to production, verify it works at 1% traffic, and roll out incrementally. This also means you can turn off a broken feature without a full rollback.

Load testing results. Before launch, run a load test against a staging environment that mirrors production. Know your p95 latency at 2x expected traffic. Know what breaks first. If you do not know your breaking point, your first production incident will discover it for you.

Checklist items for deployment:

  • Rollback procedure is documented and has been executed at least once in staging
  • Feature flags are in place for high-risk changes
  • Load test results exist showing behavior at 2x expected peak traffic
  • Container images are tagged with the commit SHA, not just “latest”
  • Deployment pipeline has a manual approval step for production (or automatic rollback on failed health checks)

Category 6: Operational Readiness

This is the category that is hardest to build, because it requires humans to do work that does not show up in code.

Runbooks. A runbook is a document that tells an on-call engineer what to do when a specific alert fires. It answers: what is the alert telling us? What are the likely causes? How do we diagnose it? How do we recover? It does not need to be long. Two paragraphs and three commands is enough to turn a 45-minute incident into a 10-minute one.

On-call rotation. Who gets paged at 2am? If the answer is “whoever happens to see the Slack message,” that is not an on-call rotation. Even a two-person team needs an explicit rotation and a way to page the right person. PagerDuty, Opsgenie, and Better Uptime all have free tiers that cover a small team.

Incident response. Your incident response process does not need to be elaborate. At minimum: one person declares the incident, one person owns communication, and there is a channel (Slack thread, incident.io, a Google Doc) where the timeline is recorded. The timeline is how you write a postmortem. The postmortem is how you avoid the same incident twice.

Checklist items for operational readiness:

  • A runbook exists for every alert that is configured
  • On-call rotation is defined with explicit coverage and escalation path
  • Incident channel and template are set up before the first incident
  • At least one incident drill (simulated or real) has been run before launch
  • Postmortem template exists so you are not creating process during an outage

Category 7: Dependencies

SLA mapping. Every external dependency your service relies on has an SLA. Your service’s availability ceiling is the product of those SLAs. If you call three services each with 99.9% uptime, your theoretical maximum uptime is 99.7%, not 99.9%. Know your dependency graph before you commit to a customer SLA.

Circuit breakers. When a downstream dependency is degraded, you want your service to fail fast rather than queue up requests that will all time out. Circuit breakers detect failure rates and open automatically, returning a fallback response instead of waiting.

// Minimal circuit breaker implementation
type CircuitState = "closed" | "open" | "half-open";

class CircuitBreaker {
  private state: CircuitState = "closed";
  private failureCount = 0;
  private lastFailureTime = 0;

  constructor(
    private readonly failureThreshold = 5,
    private readonly resetTimeoutMs = 60_000
  ) {}

  async call<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
    if (this.state === "open") {
      const elapsed = Date.now() - this.lastFailureTime;
      if (elapsed < this.resetTimeoutMs) return fallback;
      this.state = "half-open";
    }

    try {
      const result = await fn();
      if (this.state === "half-open") {
        this.state = "closed";
        this.failureCount = 0;
      }
      return result;
    } catch (err) {
      this.failureCount++;
      this.lastFailureTime = Date.now();
      if (this.failureCount >= this.failureThreshold) {
        this.state = "open";
      }
      return fallback;
    }
  }
}

Fallback behavior. For every critical external dependency, answer: what does the user experience when that dependency is unavailable? “The service crashes” is not an acceptable answer. At minimum, return a degraded but functional response or a clear error message with retry guidance.

Checklist items for dependencies:

  • SLAs are documented for all external dependencies
  • Circuit breakers or bulkheads are in place for critical external calls
  • Fallback behavior is defined and tested for each critical dependency
  • Third-party webhook endpoints validate signatures before processing payloads

Tradeoffs: What to Skip vs What is Non-Negotiable

ItemTeam SizeSkip?Why
Distributed tracing1-5 servicesDeferHigh setup cost, low immediate value. Propagate headers now, add sampling later.
Load testingAnyNeverYou must know your breaking point before users find it.
Backup restore testAnyNeverAn untested backup is not a backup.
On-call rotation1-2 engineersSimplifyOne primary, one secondary is enough. The tool matters less than the definition.
Feature flagsGreenfield launchDeferHigh operational overhead. Worth it for significant changes, overkill for initial launch.
Circuit breakersNo external callsSkipOnly necessary if you have external dependencies.
RunbooksAnyNeverOne incident without a runbook will cost more time than writing all of them.
Auth testingAnyNeverA privilege escalation bug on day one is a company-ending event.
Secrets in code checkAnyNeverFive minutes. Non-negotiable.
CORS wildcard for authenticated routesAnyNeverThis is an authorization bypass, not a configuration choice.

Scaling the Process: 2 to 20 Engineers

At 2-5 engineers: The PRR is a 90-minute checklist walkthrough over a video call the day before launch. One engineer built the system, the other runs through the checklist and asks questions. Document the results in a shared document. That is it.

At 5-10 engineers: Add a template. The engineer shipping a new service fills out the PRR document 48 hours before the planned launch date. A second engineer reviews it and signs off. Any item marked “not applicable” needs a written justification. Launches are blocked if P0 items are not resolved.

At 10-20 engineers: The PRR becomes a formal gate in your deployment process. The document lives in your incident management tool or a dedicated repo. Reviewers are drawn from a rotation (not always the same senior engineer). You start tracking which items are being skipped, which categories generate the most findings, and which findings become incidents. That data lets you tune the checklist over time.

The process does not need to be heavyweight to be effective. The worst version is no process at all. The second worst version is a 200-item checklist that teams learn to game by marking everything “done” without checking. Start with the 30-40 items above, add items after incidents, remove items that have never caught anything.


Production Considerations

Automation over documentation. Items you check manually will be skipped. Items checked by CI will not. Health endpoint tests, npm audit runs, secret scanning, and container image signing all belong in your pipeline, not on a checklist someone has to remember.

PRR findings as bugs. If you finish a PRR and have five open items marked “defer,” those are bugs, not notes. They belong in your issue tracker with priority labels, not in a document that will be read once and forgotten.

Staging parity. Many PRR items are only meaningful if your staging environment reflects production. If staging has no load balancer, no secrets manager integration, and no real database, your staging tests are fiction. Production readiness reviews will surface this gap quickly.

Review cadence beyond launch. Run a PRR whenever you introduce a new service, a new critical external dependency, or a significant change to your data model. A quarterly review of existing services is also worth the time: dependencies change, SLAs change, and the engineer who originally wrote the runbooks may have left.


The goal of a production readiness review is not to achieve perfection before launch. It is to ensure that when something goes wrong at 2am, the on-call engineer has the observability to diagnose it, the runbook to recover it, and the confidence that the blast radius is limited by the reliability work done in advance. Most production incidents are not caused by clever edge cases. They are caused by missing a basic item on a checklist that nobody wrote down.

More in DevOps

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code
DevOps ·

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code

A deep dive into Terraform's internal architecture covering HCL parsing, the expression evaluation engine, DAG-based dependency resolution, the gRPC provider plugin protocol, state mechanics, and the plan/apply lifecycle.

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container
DevOps ·

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container

A deep dive into what happens when you run docker run: Linux namespaces for process isolation, cgroups v2 for resource enforcement, overlay2 union filesystem for layered images, the OCI runtime spec and how runc spawns containers, content-addressable storage, the containerd shim architecture, and networking with veth pairs.

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop
DevOps ·

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop

A deep dive into Kubernetes internals covering the control plane architecture, etcd watch mechanics, the scheduler's filter and score phases, controller manager reconciliation loops, kubelet pod assignment, the CRI and CNI plugin models, and production considerations for resource requests, pod disruption budgets, and cluster autoscaler behavior.

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution
DevOps ·

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution

A deep dive into Docker internals covering Linux namespace isolation, cgroup resource constraints, OverlayFS copy-on-write layer mechanics, Dockerfile build cache invalidation rules, the containerd and runc OCI runtime stack, and production considerations for image hardening and startup latency.