DevOps Platform Engineering

DORA Metrics and Platform Engineering: Measure and Scale Delivery

Quick take: DORA metrics tell you how fast and how safely your software reaches users. Platform engineering is how you make that speed the default — golden paths and an internal developer platform that remove the toil every team currently re-solves. Measure first with honest, automatically-collected numbers; then build the platform that moves the metric you’re actually losing on. A platform that doesn’t move a DORA metric is a cost centre, not a product.

A mid-size company had forty engineering teams and forty different ways to ship software. Some pushed through Jenkins, some through GitHub Actions, a few still SSH’d a tarball onto a VM at 9pm. Onboarding a new service took two to three weeks of copy-pasting another team’s Jenkinsfile, guessing at the right Terraform, and filing tickets for a database, a DNS record and a TLS certificate. Nobody could answer the CTO’s simplest question — “are we getting faster or slower?” — because there was no shared definition of “shipped,” let alone a number. The first real fix was not a tool. It was a measurement: instrument the four DORA metrics so the organisation could see, per team, how often it deployed, how long a change took to reach production, how often a deploy broke something, and how fast it recovered. Only then did a small platform team build golden paths — a service template, a reusable pipeline, default observability, a developer portal — and time-to-first-deploy for a new service fell from three weeks to about ninety minutes. The DORA numbers moved because the platform removed the specific bottleneck the numbers had exposed.

This article is the advanced, end-to-end treatment of that loop. DORA — the DevOps Research and Assessment program, the research behind Accelerate and the annual State of DevOps reports — distilled software delivery performance into four metrics that, unusually for engineering metrics, correlate with organisational outcomes rather than gaming-prone vanity. You will learn precisely what each metric is, the exact events you must capture to compute it honestly, the elite/high/medium/low performance bands and what they mean, and the real queries (SQL over a deployments table, KQL over pipeline telemetry, PromQL over a deploy counter) that turn raw events into a dashboard. Then we cross the bridge from measuring delivery to engineering it: platform engineering as a discipline, the Internal Developer Platform (IDP), golden paths, Backstage and software catalogs, Team Topologies as the org model that makes a platform team work, the anti-patterns that turn a platform into a new bottleneck, and how to prove — with the same DORA metrics — that the platform paid for itself.

By the end you will stop treating “DevOps maturity” as a feeling and treat it as four instrumented numbers plus a product (the platform) you invest in to move them. You will know why deployment frequency without change failure rate is a trap, why lead time measured from “ticket created” is meaningless and from “first commit” is gold, why a platform built before you understand team pain becomes shelfware, and how the elite performers actually operate — small batches, trunk-based development, automated testing, progressive delivery and a platform that makes the safe path the easy path.

What problem this solves

Two pains, tightly coupled. The first is flying blind on delivery performance. Leadership wants to know whether an “investment in DevOps” or a reorg or a new CI system actually made delivery better, and engineering can only offer anecdotes. Without DORA metrics you cannot tell a genuinely fast, safe team from a team that ships rarely but loudly, you cannot localise where delivery is slow (is it review latency, a flaky test suite, a manual change-approval board, or a deploy that takes four hours?), and you cannot defend platform or tooling spend with a before/after. Worse, teams optimise the thing they can see — story points, lines of code, PRs merged — none of which correlate with delivering value safely, and several of which actively reward the wrong behaviour.

The second pain is every team re-solving the same hard infrastructure problems. In an organisation of any size, dozens of teams independently figure out how to build a container, wire a pipeline, provision a database, get a TLS certificate, set up logging and metrics, manage secrets, and pass a security review. The result is weeks of onboarding per service, wild inconsistency in how things are secured and observed, a long tail of bespoke pipelines nobody understands, and senior engineers burned on toil instead of product. Platform engineering centralises the genuinely hard, genuinely shared parts into a self-service platform — but only the parts that should be shared — while preserving team autonomy through golden paths the teams can step off when they have a real reason to.

The coupling is the whole point. DORA metrics tell you which bottleneck to attack; the platform is how you attack it at scale; and the metrics then tell you whether the platform worked. Build the platform without the metrics and you’ll automate the wrong thing beautifully. Measure without building the platform and you’ll have a dashboard full of red numbers and no lever to move them.

Who hits this: any organisation past roughly five to ten teams, where the cost of not having shared paths starts to exceed the cost of building and running them; any team whose leadership is asking “are we fast?” and getting hand-waving; and any platform team that built an IDP nobody adopted because they never measured the pain they were supposed to relieve.

To frame the field before the deep dive, here is the loop this article teaches — the four questions, the metric that answers each, and the platform capability that typically moves it:

Delivery question DORA metric that answers it Primary bottleneck it exposes Platform capability that usually moves it
How often do we ship to users? Deployment frequency Big batches, manual gates, fear of deploying Golden-path CI/CD, trunk-based dev, one-click deploy
How long from code to production? Lead time for changes Slow review, flaky tests, change-approval boards Fast pipelines, automated tests, paved-road templates
How often does a deploy break things? Change failure rate No tests, no progressive delivery, big bang Quality gates, canary/blue-green, automated rollback
How fast do we recover when it breaks? Time to restore service (MTTR) No observability, manual rollback, no runbooks Default observability, instant rollback, SLOs

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already be fluent in modern delivery mechanics: what a CI/CD pipeline is and what its stages do, the difference between blue-green, canary and rolling deployments, how progressive delivery and feature flags decouple deploy from release, and the three pillars of observability — logs, metrics, traces and SLOs. You should know what Infrastructure as Code buys you and roughly how GitOps with Argo CD or Flux reconciles desired state from Git, because both are load-bearing for a real platform.

This article sits at the top of the DevOps track — it is the measurement-and-scale layer that sits above the individual practices. DORA metrics are the scoreboard for everything those upstream articles teach; platform engineering is the operating model that lets you apply those practices consistently across many teams instead of one. If CI/CD platforms and Git branching strategies are the tools and tactics, this is the strategy and scoreboard.

A quick map of who owns and consumes each layer, so you wire the right people to the right number:

Layer What lives here Who owns it Which DORA metric it most affects
Version control / review Branching, PRs, review SLAs Stream-aligned teams Lead time (review latency)
CI (build + test) Build, unit/integration tests, scans, quality gates Teams + platform (shared templates) Lead time, change failure rate
CD (deploy) Deploy strategy, approvals, rollback Teams (on golden-path CD) Deployment frequency, time to restore
Runtime / observability Logs, metrics, traces, SLOs, alerting Platform provides defaults; teams own SLOs Time to restore, change failure rate
Internal Developer Platform Templates, catalog, self-service infra Platform team (as a product) All four (it removes the bottleneck)
Org design Team boundaries, cognitive load, interaction modes Engineering leadership All four (indirectly, decisively)

Core concepts

Six mental models make every later section obvious.

DORA measures the system, not the individual. The four metrics describe how the delivery system of a team or product performs — not how productive a person is. Used per-engineer they become surveillance and get gamed instantly; used per-team or per-service over time they are a remarkably honest signal of delivery health. The research finding that matters: high throughput and high stability are not a trade-off — elite performers are better at both at once, because the same practices (small batches, automation, fast feedback) improve speed and safety together. Any framing of “we slowed down to be safe” usually means you lack the automation that makes safe also fast.

Throughput and stability are two pairs, read together. Deployment frequency and lead time for changes are the throughput (velocity) pair — how much, how fast. Change failure rate and time to restore service are the stability pair — how safe, how resilient. A single metric in isolation lies: high deployment frequency with a 40% change failure rate is a fire, not a triumph; a 2% change failure rate achieved by deploying once a quarter is stagnation dressed as quality. You read the four as a set, and you read the two pairs against each other.

Every metric is a clock between two events — define both ends or the number is fiction. Each metric is the duration or count between two timestamped events. Lead time for changes is the clock from code committed to that code running in production — not from “ticket created” (which measures your backlog, not your delivery) and not to “merged to main” (which stops the clock before the value reaches a user). Time to restore is from a user-impacting incident begins to service is restored. Get the two endpoints right and the metric is trustworthy and automatable; get them wrong and you’ll optimise a number that doesn’t mean what you think.

A deployment is to production and user-facing. Counting CI runs, staging deploys, or merges as “deployments” inflates deployment frequency into vanity. The honest definition: a successful deployment of code to the production environment that could reach users (whether or not a feature flag gates the actual release). This is exactly why progressive delivery matters — it lets you deploy frequently (the metric you want high) while controlling release (the risk you want low) independently.

Platform engineering is product management for infrastructure. A platform is an internal product whose customers are your own developers; its job is to reduce their cognitive load and remove toil by offering self-service golden paths — the well-supported, secure, observable default way to do a common thing (create a service, ship it, get a database, see its logs). “Golden path,” “paved road” and “well-lit path” are synonyms. The platform is opt-in by being the easiest option, not mandatory by decree — the moment it’s a mandate you’ve built a gate, not a path.

Build the platform from measured pain, run it as a product. The platform’s backlog comes from the DORA metric you’re losing on and from listening to developers, not from what the platform team finds fun to build. It has users, a roadmap, adoption metrics, on-call, docs and a feedback loop — or it becomes shelfware. This is the single biggest determinant of whether platform engineering succeeds or fails, and it’s an operating-model decision, not a tooling one.

The DORA four in one table

Pin down all four before the deep sections. This is the mental model side by side; the per-metric sections expand each with measurement detail:

Metric What it measures Pair Start event Stop / count event Honest definition gotcha
Deployment frequency How often you deploy to prod Throughput — (it’s a count over time) A successful production deploy Don’t count CI runs or staging deploys
Lead time for changes Commit → running in prod Throughput First commit of the change That change live in production Not from “ticket”; not to “merged”
Change failure rate % of deploys causing a failure Stability A production deploy It caused a degradation/incident/rollback “Failure” must mean user-impacting, defined upfront
Time to restore service Incident start → restored Stability User-impacting incident begins Service restored to users Detection time counts; “ticket closed” ≠ restored

The performance bands (elite / high / medium / low)

DORA’s research clusters performers into four bands. The exact numbers shift year to year in the State of DevOps report, but the shape is stable and is what you should internalise — the gap between elite and low is orders of magnitude, not percentages. Use these as orientation, not gospel; your own trend over time matters far more than hitting a global band:

Metric Elite High Medium Low
Deployment frequency On-demand (multiple deploys/day) Between once/day and once/week Between once/week and once/month Between once/month and once every 6 months
Lead time for changes Less than one hour Between one day and one week Between one week and one month Between one month and six months
Change failure rate 0–15% 16–30% (varies by report) similar band Higher / more variable
Time to restore service Less than one hour Less than one day Between one day and one week Between one week and one month

How to read the bands without fooling yourself — the traps each one hides:

Band-reading question The trap The honest reading
“We deploy 10×/day — we’re elite!” Frequency alone is vanity Only elite if change failure rate and restore time are also good
“Our lead time is 30 minutes” Measured to merge, not to prod Re-measure to running-in-production; it’s usually much longer
“Change failure rate is 2%” Achieved by deploying rarely Cross-check deployment frequency; rare + low CFR = stagnation
“MTTR is 20 minutes” Clock starts at ticket, not at impact Start at detection/impact; include the time you didn’t notice
“We’re medium and stuck” Comparing to a global band Compare to your own last quarter; trend beats absolute band

Metric 1 — Deployment frequency

Deployment frequency is how often your team successfully releases code to production. It is the simplest metric to state and the easiest to fake, so the definition discipline matters most here. It is a count over a window (deploys per day/week), and the canonical “elite” expression is on-demand: whenever a change is ready, it ships, often many times a day, with no batching and no calendar.

Why it’s the headline metric: frequency is a proxy for batch size. Teams that deploy often are, by necessity, shipping small changes — and small changes are easier to review, test, reason about, and roll back. A team deploying once a month is shipping a giant batch of coupled changes whose blast radius on failure is huge and whose root cause on failure is a needle in a haystack. So “deploy more often” is really “make your batches small,” which is the root cause of most other improvements.

What counts and what doesn’t — get this wrong and every downstream number is polluted:

Event Counts as a deployment? Why
Successful deploy of code to production Yes This is the definition
Deploy to staging / QA / pre-prod No Not user-reachable; inflates the number
A CI build that passed No A build is not a deploy
A merge to main No (unless main auto-deploys to prod) Merge ≠ release to users
Config/feature-flag flip with no code deploy Judgment call Count if it changes prod behaviour and you want release-frequency too
A failed/rolled-back production deploy Count separately (it feeds change failure rate) It happened, but it’s a failed deploy
Re-deploy of the same artifact (no change) No No new value reached users

How to measure deployment frequency

The cleanest source is a deployments table you write a row to from the final, successful production-deploy step of every pipeline. This decouples the metric from any one CI tool and survives tool migrations. The minimal schema:

Column Type Example Why you need it
deployment_id uuid 7f3a… Idempotency / joins to incidents
service text checkout-api Per-service breakdown
team text payments Per-team rollup
environment text production Filter to prod only
commit_sha text a1b2c3d Join to VCS for lead time
deployed_at timestamptz 2026-06-23T09:14:02Z The count’s timestamp
status text succeeded / failed Frequency counts succeeded; CFR uses failed
version text 1.8.3 Correlate with incidents

From that table, deployment frequency is a trivial aggregation. Daily deploys per service over the last 30 days:

-- Deployment frequency: successful production deploys per service per day (last 30 days)
SELECT
  service,
  date_trunc('day', deployed_at) AS day,
  count(*) AS deploys
FROM deployments
WHERE environment = 'production'
  AND status = 'succeeded'
  AND deployed_at >= now() - interval '30 days'
GROUP BY service, day
ORDER BY service, day;

If you’d rather emit a metric than query a table, increment a Prometheus counter from the deploy step and let your dashboard do the rate:

# Deploys per day, per service, over the last 30 days (counter incremented on each prod deploy)
sum by (service) (increase(deploy_total{environment="production", status="succeeded"}[30d])) / 30

If you have no deployments table yet, you can derive frequency from the CI/CD API. GitHub exposes Deployments and deployment statuses; Azure DevOps exposes pipeline runs and release deployments; GitLab exposes deployments per environment. Pull successful production deployments and count them:

# GitHub: count successful production deployments in a repo over a window
gh api -X GET "repos/acme/checkout-api/deployments?environment=production&per_page=100" \
  --jq '[.[] | select(.created_at > "2026-05-24")] | length'

The source options, with the trade-off of each:

Source How you get the events Pros Cons / gotcha
Dedicated deployments table Insert a row from the deploy step Tool-agnostic, exact, survives migrations You must instrument every pipeline once
Prometheus deploy counter increase() on a counter Lives with your other metrics; cheap dashboards Counter resets; needs care over restarts
GitHub Deployments API gh api .../deployments No extra plumbing if you already use Deployments Only as accurate as your Deployments usage
Azure DevOps runs/releases REST API / az pipelines Native to Azure pipelines Distinguish stage success vs pipeline success
GitLab environments/deployments REST/GraphQL API Per-environment, native “Deployment” semantics vary by project setup
Git tags (release tags) Count tags matching a pattern Dead simple if you tag every release Only works if tagging is disciplined and prod-only

Metric 2 — Lead time for changes

Lead time for changes is the time from a change being committed to that change running in production. It is the metric most often mis-measured, and the mis-measurement always flatters you. The two correct endpoints are first commit of the change and that commit live in production; everything between — review, CI, queueing, approvals, the deploy itself — is the value-stream latency this metric exposes.

Why the endpoints matter so much: starting the clock at “ticket created” measures how long work sat in your backlog (a planning concern, not a delivery one) and makes a fast-delivering team look slow because a ticket aged for two weeks before anyone picked it up. Stopping the clock at “merged to main” hides the often-large gap between merge and a real production release — the deploy queue, the change-approval board, the weekly release train. Lead time is the metric that surfaces your delivery pipeline’s latency, so both ends must be on the delivery pipeline: code in, value out.

Lead time decomposes into stages, and decomposing it is how you find the bottleneck — the whole point of measuring it. A typical breakdown and where each stage’s time hides:

Stage of lead time Clock from → to Where the time goes Platform/practice lever
Coding (pre-commit) (not counted) (out of scope by definition)
Review wait First commit → PR approved Reviewer latency, big PRs, timezone gaps Small PRs, review SLAs, code owners
CI PR opened → checks green Slow/flaky tests, serial stages, no caching Fast parallel tests, caching, golden-path CI
Merge → deploy queue Merged → deploy starts Release trains, manual approvals, CAB Continuous deployment, automate approvals
Deploy Deploy starts → live in prod Slow rollout, manual steps, big artifacts One-click/automated deploy, progressive rollout
(optional) release Deployed → flag flipped on Decoupled by feature flags Progressive delivery (separate metric)

How to measure lead time for changes

You join two facts: the first commit timestamp of the change (from your VCS) and the production deploy timestamp of the commit that contains it (from your deployments table). The simplest robust approximation many teams use: for each production deployment, take the commits newly included since the previous production deployment, and compute deployed_at - commit.authored_at for each; the median (or 50th/85th percentile) over a window is your lead time.

With a deployments table that records commit_sha, and a commits table (or a VCS API call) giving each commit’s authored_at, the median lead time per service:

-- Median lead time for changes (commit authored → deployed to prod), last 30 days, per service
WITH deploy_pairs AS (
  SELECT d.service,
         d.deployed_at,
         c.authored_at,
         EXTRACT(EPOCH FROM (d.deployed_at - c.authored_at)) / 3600.0 AS lead_hours
  FROM deployments d
  JOIN deployment_commits dc ON dc.deployment_id = d.deployment_id
  JOIN commits c            ON c.sha = dc.commit_sha
  WHERE d.environment = 'production'
    AND d.status = 'succeeded'
    AND d.deployed_at >= now() - interval '30 days'
)
SELECT service,
       percentile_cont(0.5)  WITHIN GROUP (ORDER BY lead_hours) AS p50_lead_hours,
       percentile_cont(0.85) WITHIN GROUP (ORDER BY lead_hours) AS p85_lead_hours
FROM deploy_pairs
GROUP BY service
ORDER BY p50_lead_hours DESC;

If your pipeline telemetry lands in a log/metrics store (e.g. an Azure DevOps or GitHub Actions export into a Log Analytics workspace), you can compute the same thing in KQL by joining a PipelineRun table (carrying commit and deploy times) and summarising percentiles:

// Lead time for changes from pipeline telemetry: commit time → successful prod deploy
PipelineRun
| where environment == "production" and result == "succeeded"
| where deployFinished > ago(30d)
| extend leadHours = datetime_diff('minute', deployFinished, commitAuthoredTime) / 60.0
| summarize p50=percentile(leadHours, 50), p85=percentile(leadHours, 85), deploys=count() by service
| order by p50 desc

Measurement choices that change what the number means — decide each deliberately:

Choice Option A Option B Recommendation
Start event First commit of the change PR opened First commit (DORA-canonical)
End event First prod deploy containing it Merge to main First prod deploy
Aggregation Median (p50) Mean p50 + p85 (mean is skewed by outliers)
Per-commit vs per-PR Each commit Each PR/change Per-change is more intuitive; per-commit is simpler
Window 7 / 30 / 90 days 30 days for a stable trend, 7 for fast feedback
Weekend/holiday handling Wall-clock Business hours only Wall-clock (users don’t care about your calendar)

A worked number: if a change is committed Monday 10:00, the PR waits a day for review (slow reviewer), CI is 25 minutes, it merges Tuesday 11:00, then waits for Thursday’s release train and deploys Thursday 14:00 — the lead time is ~76 hours, of which only 25 minutes was CI. The decomposition instantly tells you the levers are review latency and the release train, not your test speed — which you’d never have known from the single aggregate number.

Metric 3 — Change failure rate

Change failure rate (CFR) is the percentage of deployments to production that result in a degraded service requiring remediation — a rollback, a hotfix, a patch, or an incident. It is the first of the two stability metrics, and the one whose definition you must nail down before you start measuring, because “failure” is subjective unless you make it concrete.

The honest definition has two halves. First, “failure” means user-impacting degradation or an event that required unplanned remediation — not “a test failed in CI” (that’s pre-production and is good; it caught the problem), and not “we found a minor bug next sprint.” Second, it is a rate: failures divided by total deployments over the same window. A team that deploys 100 times and rolls back 5 has a 5% CFR; a team that deploys twice and rolls back once has 50% — frequency context is essential, which is why you read CFR next to deployment frequency, never alone.

What counts as a “failed change” — agree this with your team and write it down:

Event after a deploy Counts toward CFR? Note
Rollback to the previous version Yes The canonical signal
Hotfix/patch deployed to fix the deploy Yes Remediation = failure
A production incident attributed to the deploy Yes Link incident → deployment
Feature flag turned off to stop user impact Yes Release-level failure
A test failed in CI (caught pre-prod) No This is the system working
A pre-existing bug surfaced, unrelated to the deploy No Not caused by this change
Planned rollback for a business reason Judgment call Usually no, if no degradation occurred

How to measure change failure rate

You need two things you already have once you’ve built the deployments table: a count of production deployments, and a way to mark which deployments failed (caused remediation). The cleanest approach is to link your incident tracker (PagerDuty, Opsgenie, Jira incidents, an incidents table) to deployments — by service and time window, or better, by an explicit caused_by_deployment_id. Then CFR is failed deployments over total deployments:

-- Change failure rate per service (last 30 days):
-- a deploy is "failed" if it was rolled back OR an incident references it
WITH prod AS (
  SELECT * FROM deployments
  WHERE environment = 'production' AND status = 'succeeded'
    AND deployed_at >= now() - interval '30 days'
),
failed AS (
  SELECT DISTINCT p.deployment_id, p.service
  FROM prod p
  LEFT JOIN incidents i ON i.caused_by_deployment_id = p.deployment_id
  WHERE p.was_rolled_back = true OR i.id IS NOT NULL
)
SELECT p.service,
       count(*)                              AS total_deploys,
       count(f.deployment_id)                AS failed_deploys,
       round(100.0 * count(f.deployment_id) / count(*), 1) AS change_failure_rate_pct
FROM prod p
LEFT JOIN failed f ON f.deployment_id = p.deployment_id
GROUP BY p.service
ORDER BY change_failure_rate_pct DESC;

If incidents and deploys live in a metrics/log store, the KQL equivalent joins a Deployments table to an Incidents table on service within a short post-deploy window (a deploy that precedes an incident on the same service by less than, say, 60 minutes is a candidate cause):

// Change failure rate from telemetry: deploys joined to incidents within 60 min on the same service
let window = 30d;
let deploys = Deployments | where environment == "production" and result == "succeeded" and deployedAt > ago(window);
let fails = deploys
  | join kind=leftouter (Incidents | where startedAt > ago(window)) on service
  | where isnotempty(incidentId) and (startedAt - deployedAt) between (0min .. 60min)
  | distinct deploymentId, service;
deploys
| summarize total = count() by service
| join kind=leftouter (fails | summarize failed = count() by service) on service
| extend cfr_pct = round(100.0 * coalesce(failed, 0) / total, 1)
| project service, total, failed = coalesce(failed, 0), cfr_pct
| order by cfr_pct desc

The hard part is attribution — knowing which deploy caused which incident. The options, worst to best:

Attribution method How it works Accuracy Effort
Time-window heuristic Incident within N minutes of a deploy on the same service Low–medium (false positives) Low
Manual tagging Responder tags the incident with the deploy/version Medium–high Medium (process discipline)
Explicit caused_by_deployment_id Incident record stores the deployment it was traced to High Medium (post-incident step)
Version correlation Incident’s error telemetry carries service.version matching a deploy High Medium (requires version in telemetry)
Automated rollback signal An automated rollback is the failure event High Low (if you have automated rollback)

The most reliable signal of all is automated rollback: if your progressive-delivery system rolls back a canary on a bad SLO, that rollback is an unambiguous, machine-recorded failed change — no human attribution needed. This is one more reason elite teams’ stability metrics are trustworthy: their tooling records the failures for them.

Metric 4 — Time to restore service

Time to restore service (often written MTTR, mean time to restore/recovery) is how long it takes to restore service after a user-impacting production failure. It is the second stability metric and answers the question that actually matters to users: when it breaks, how fast do you make it right? Crucially, the clock starts when the impact begins (or is detected), not when a ticket is opened or when an engineer happens to look — the time you didn’t notice is part of your restore time, and pretending otherwise hides your worst gap.

The endpoints: start at incident begins / is detected (user impact starts), stop at service restored (users are served correctly again — which may be before the full root-cause fix, because a rollback or failover restores service while the real fix lands later). Distinguish restore (users are OK again) from resolve (the underlying defect is fixed); DORA measures restore. A team that can roll back in two minutes has an elite restore time even if the proper fix takes two days.

Restore time decomposes, and the decomposition tells you where to invest:

Phase of restore Clock from → to What dominates it Lever to shrink it
Detection Impact starts → alert fires Missing/blind alerting, no SLOs SLO-based alerting, synthetic checks
Acknowledgement Alert fires → human responding On-call paging, escalation gaps Clear on-call, fast paging, runbooks
Diagnosis Responding → cause localised Poor observability, no traces Logs/metrics/traces, deploy markers
Mitigation Cause known → service restored Manual rollback, no instant lever One-click/automated rollback, failover
(later) resolution Restored → defect fixed (not part of MTTR) Post-incident review

How to measure time to restore service

The source is your incident tracker: each incident needs a started_at (impact began/detected) and a restored_at (service restored). MTTR is the central tendency of restored_at - started_at over a window — and you should report the median and p90, not just the mean, because one nightmarish incident skews the mean and hides your typical recovery:

-- Time to restore service: median and p90 over incidents in the last 90 days, per service
SELECT
  service,
  count(*) AS incidents,
  percentile_cont(0.5) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (restored_at - started_at)) / 60.0
  ) AS p50_restore_minutes,
  percentile_cont(0.9) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (restored_at - started_at)) / 60.0
  ) AS p90_restore_minutes
FROM incidents
WHERE started_at >= now() - interval '90 days'
  AND restored_at IS NOT NULL
GROUP BY service
ORDER BY p90_restore_minutes DESC;

A practical subtlety: where does started_at come from? If you set it to the moment a human created the incident ticket, you’ve erased detection time. Better is to derive it from the first breaching data point — the timestamp your SLO/alerting first saw the error budget burn — which you can pull from your monitoring system. Tying restore time to the SLO that defines “impact” makes it objective and removes the temptation to start the clock late.

The data-quality choices that decide whether your MTTR is honest:

Choice Tempting (flattering) option Honest option
started_at When the ticket was created When impact began / SLO first breached
restored_at When the post-mortem was filed When users were served correctly again
Which incidents count Only the ones we remembered to log All user-impacting incidents (enforce logging)
Central tendency Mean only Median + p90 (mean hides the long tail)
Restore vs resolve Time to full root-cause fix Restore (rollback/failover counts)

Reading the four together — and the guardrails

The reason DORA endures where other engineering metrics fail is that the four resist gaming as a set. Push deployment frequency up by shipping reckless big-bang changes and your change failure rate spikes. Drive change failure rate to zero by deploying once a quarter and your frequency and lead time collapse. Hit a great lead time by skipping tests and your CFR and restore time blow out. The set has internal tension that keeps any single number honest — which is exactly why you must always look at all four, and at the two pairs against each other.

The classic two-by-two — throughput against stability — and what each quadrant means:

High stability (low CFR, fast restore) Low stability (high CFR, slow restore)
High throughput (frequent, fast) Elite — small batches + automation + progressive delivery Fragile / reckless — fast but breaking things; add tests, gates, rollback
Low throughput (rare, slow) Cautious / stuck — safe by deploying rarely; reduce batch size, automate Low performer — slow and fragile; start with the worst metric

Even four metrics can be gamed locally, so production teams add guardrail metrics and the now-commonly-cited fifth metric, reliability (how well the service meets its SLOs / user expectations), to keep the optimisation honest:

Risk if you over-optimise… …you might harm Guardrail metric to watch
Deployment frequency Stability, code quality Change failure rate, defect escape rate
Lead time Test coverage, review rigour CFR, % changes with tests, review depth
Change failure rate (drive to 0) Throughput (deploy too rarely) Deployment frequency, batch size
Time to restore (optimise rollback) Root-cause fixing (mask recurring bugs) Incident recurrence rate, defect resolution time
All four (per-individual) Trust, collaboration Never measure per-individual; team-level only
Speed at all costs User happiness Reliability / SLO attainment (the 5th metric)

A note on how to collect this honestly: the gold standard is automated, system-derived metrics (events emitted by your pipeline, VCS and monitoring) rather than survey-based self-report. Surveys (the original DORA research method) are valid for benchmarking culture and for organisations with no instrumentation, but for a live operational dashboard you want the four computed from real events — that’s what the queries above do, and it’s what removes the temptation to round your own numbers up.

Instrumenting the pipeline end to end

Now make it real. The architecture is: every pipeline emits deployment events; your VCS provides commit times; your incident tracker provides incident start/restore; a small metrics store (a database table, or Prometheus + a log store) holds the events; and a dashboard computes the four metrics and trends them. You can build this yourself, or adopt an off-the-shelf collector.

The events you must emit, from where, and what each metric consumes:

Event Emitted by Carries Feeds which metric
deployment.started CD job (prod stage) service, env, commit_sha, version, time (timing/decomposition)
deployment.finished CD job (prod stage) + status (succeeded/failed) Deployment frequency, CFR
commit VCS webhook / API sha, authored_at, author, PR Lead time
pr.merged VCS webhook sha, merged_at Lead-time decomposition
incident.opened Incident tool / alerting service, started_at, severity Time to restore, CFR
incident.restored Incident tool restored_at Time to restore
rollback CD job / progressive-delivery deployment_id, time CFR (unambiguous failure)

Emitting the deploy event is one block at the end of your production deploy job. In GitHub Actions, posting to your own collector:

# .github/workflows/deploy.yml — final step of the production deploy job
      - name: Record deployment for DORA metrics
        if: always()                      # record success AND failure
        run: |
          curl -fsS -X POST "$DORA_COLLECTOR_URL/deployments" \
            -H "Authorization: Bearer ${{ secrets.DORA_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d "$(jq -n \
                  --arg svc   'checkout-api' \
                  --arg env   'production' \
                  --arg sha   '${{ github.sha }}' \
                  --arg ver   '${{ github.ref_name }}' \
                  --arg status '${{ job.status }}' \
                  '{service:$svc, environment:$env, commit_sha:$sha, version:$ver, status:$status, deployed_at:(now|todateiso8601)}')"

The Azure DevOps equivalent, as a final pipeline step:

# azure-pipelines.yml — record the production deployment
  - task: Bash@3
    displayName: Record deployment for DORA metrics
    condition: always()
    inputs:
      targetType: inline
      script: |
        curl -fsS -X POST "$(DORA_COLLECTOR_URL)/deployments" \
          -H "Authorization: Bearer $(DORA_TOKEN)" \
          -H "Content-Type: application/json" \
          -d "{\"service\":\"checkout-api\",\"environment\":\"production\",\"commit_sha\":\"$(Build.SourceVersion)\",\"version\":\"$(Build.BuildNumber)\",\"status\":\"$(Agent.JobStatus)\",\"deployed_at\":\"$(date -u +%FT%TZ)\"}"

Build vs buy — the realistic options and when each fits:

Approach What it is Best when Watch-out
Roll your own (table + queries) Deployments/incidents tables + SQL dashboards You want full control and the schema above You own the plumbing and upkeep
Prometheus + Grafana Deploy counter + incident gauge + panels You already run Prometheus/Grafana Counter semantics; not great for per-change lead time
Four Keys (open source) Google’s reference DORA pipeline (BigQuery + dashboards) GCP-centric, want a reference build Opinionated; some assembly
CD platform built-in (e.g. GitLab, Azure DevOps Analytics) Native DORA dashboards You’re all-in on one platform Only sees that platform’s data
Backstage DORA plugin DORA shown in the developer portal You already run Backstage Depends on a backing data source
Commercial (LinearB, Sleuth, Faros, Cortex, etc.) Managed metric collection + insights You want it now, low effort Cost; data leaves your estate

The non-negotiable principle whichever you pick: derive the metrics from real events, scope them to team/service, and never to an individual. The moment a manager ranks engineers by these numbers, the numbers die — people optimise the metric, not the outcome, and your honest scoreboard becomes theatre.

From measuring to engineering delivery: platform engineering

You now have four honest numbers and, almost always, one that’s clearly worst. Improving it for one team is a project. Improving it across forty teams — without forty separate efforts — is platform engineering: building an internal product that makes the fast, safe path the default path for every team.

The core idea is cognitive load reduction. A modern stream-aligned team is asked to own its service end to end — code, build, pipeline, infrastructure, security, observability, on-call. That’s an enormous surface, and most of it is undifferentiated heavy lifting identical across teams. Platform engineering extracts the shared, hard parts into self-service capabilities so a product team can focus on its actual product. The deliverable is a set of golden paths: paved, supported, secure-by-default routes for the common things — create a new service, ship it, give it a database, see its logs, rotate its secrets — that a developer can self-serve in minutes without filing a ticket or reading a wiki.

What belongs on the platform versus what stays with the team — the single most important judgment call, because getting it wrong gives you either a useless platform (too little) or a straitjacket (too much):

Capability Platform provides Team owns Rationale
New-service scaffolding The template + scaffolder Choosing to use it; filling in business logic Shared shape, team’s content
CI/CD pipeline Reusable pipeline templates Their pipeline config (built from templates) Shared mechanics, team’s stages
Infrastructure (DB, queue, cache) Self-service modules + provisioning Requesting + configuring theirs Shared IaC, team’s resources
Observability Default dashboards, log/trace wiring, SLO scaffolding Their service’s SLOs and alerts Shared instrumentation, team’s targets
Secrets management The secrets store + access pattern Their secrets and rotation Shared mechanism, team’s data
Security/compliance Baked-in scanning, policy, golden-path approvals Fixing their findings Shared guardrails, team’s code
Business logic (nothing) Everything Never on the platform
Service-specific architecture (nothing) Everything Team autonomy

The trap on both ends, stated plainly:

Failure mode What it looks like Why it happens The fix
Platform too thin Teams still re-solve infra; no adoption Built abstractions nobody needed Build from measured pain + user research
Platform too thick (golden cage) Advanced teams blocked; escape hatches missing Mandate over enablement Make paths optional; provide escape hatches
Platform-as-gatekeeper Self-service becomes a ticket queue Run as ops/approval function, not product Self-service APIs; product operating model
Platform-as-tech-museum Cool tech, no users Built what was fun, not what was needed Adoption metrics; sunset unused capabilities

The Internal Developer Platform (IDP)

An Internal Developer Platform is the productised layer through which developers self-serve those golden paths. It is not one tool; it’s a thin, opinionated integration over your existing infrastructure that gives developers a coherent, self-service interface and gives the platform team a place to encode standards. A useful way to reason about an IDP is by its planes (popularised by the “platform engineering reference architecture” mental model):

Plane What it does Concrete examples
Developer control plane The interface developers actually touch Portal (Backstage), CLI, service templates, Git as the API
Integration & delivery plane Glue that turns intent into action CI/CD, GitOps controllers (Argo CD/Flux), image registry, secrets operator
Resource plane The real infrastructure being provisioned Kubernetes, managed DBs, queues, cloud accounts, networking
Monitoring & logging plane Feedback on what’s running Metrics, logs, traces, SLO/error-budget tooling
Security plane Guardrails across the others Policy-as-code (OPA/Kyverno), image scanning, secret management, RBAC

The IDP’s job is to let a developer express intent (“I need a new payments worker with a Postgres database and standard observability”) and have the platform realise it through those planes — provisioning the infra, wiring the pipeline, registering the service in the catalog, attaching dashboards — without the developer touching the resource plane directly. The interface to that intent is typically one of:

IDP interface style How developers interact Strength When to choose it
Portal-driven (Backstage) Web UI: browse catalog, run templates Discoverable, great for many teams Large orgs; non-expert users
Git-driven / GitOps Commit a manifest; controllers reconcile Auditable, declarative, no UI to maintain Teams comfortable with Git as the API
CLI-driven platform create service … Fast for power users; scriptable Engineer-heavy orgs
API-driven Call the platform’s API directly Composable; build your own UX on top When integrating into other tooling
Crossplane / control-plane Kubernetes CRDs as the platform API Cloud-native, reconciled, extensible K8s-centric platforms

Most mature platforms combine these — a Backstage portal for discovery and scaffolding, GitOps underneath for the actual reconciliation, and a CLI for power users — all hitting the same underlying capabilities.

Golden paths in practice

A golden path is only “golden” if it is genuinely the easiest option and it bakes in the standards you want by default. Concretely, the “create a new service” golden path should, from a single template invocation, produce: a repository with a sensible service skeleton, a working CI/CD pipeline (built from the shared templates), default observability (logs/metrics/traces wired, a starter dashboard, an SLO stub), secrets wired via the platform’s store, security scanning enabled, and the service registered in the software catalog. The developer fills in business logic; everything around it is already paved.

The canonical golden paths and what each removes:

Golden path What the developer does What the platform does automatically Toil it removes
Create a service Run the template, name it, pick a type Repo, pipeline, dashboards, SLO stub, catalog entry, secrets wiring Days of copy-paste scaffolding
Ship a change Merge to main Build, test, scan, deploy via golden-path CD, record DORA event Re-inventing the deploy pipeline
Get a database Request via portal/manifest Provision (IaC module), inject connection via secrets, register Tickets + bespoke Terraform
Observe a service (nothing extra) Default dashboards, log/trace correlation, alert templates Wiring observability from scratch
Rotate a secret Update in the secrets store Propagate to running workloads Hand-rolled secret distribution
Roll back One click / automated on SLO breach Revert to previous version, record failure Manual, slow, scary rollbacks

The discipline that keeps a golden path healthy — it is a product feature, not a one-time scaffold:

Backstage and the software catalog

Backstage — the open-source developer-portal framework Spotify created and donated to the CNCF — is the most common implementation of the developer control plane. It is not “the platform”; it’s the front door to the platform. Its three load-bearing features:

Backstage feature What it does Why it matters
Software Catalog A registry of all services/components/APIs/resources, each described by a catalog-info.yaml and an owner Answers “what do we have, who owns it, how do I find it?” — the end of tribal knowledge
Software Templates (Scaffolder) Parameterised templates that generate a new repo/service from the golden path Turns “create a service” into a form, not a three-week odyssey
TechDocs Docs-as-code rendered in the portal next to each component Docs live with the code and are actually discoverable

The catalog is anchored by a small descriptor committed to each repo. A minimal catalog-info.yaml for a service:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: checkout-api
  description: Checkout and payment orchestration service
  annotations:
    github.com/project-slug: acme/checkout-api
    backstage.io/techdocs-ref: dir:.
spec:
  type: service
  lifecycle: production
  owner: team-payments         # resolves to a Group entity in the catalog
  system: commerce
  dependsOn:
    - resource:postgres-checkout
    - component:identity-api

A software template (scaffolder) that is the “create a service” golden path — collect inputs, fetch a skeleton, push a repo, register it:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golden-path-service
  title: New production service (golden path)
spec:
  parameters:
    - title: Service details
      required: [name, owner]
      properties:
        name:  { type: string, description: Service name (kebab-case) }
        owner: { type: string, ui:field: OwnerPicker }
  steps:
    - id: fetch
      name: Fetch skeleton
      action: fetch:template
      input:
        url: ./skeleton            # repo skeleton: pipeline, Dockerfile, SLO stub, catalog-info
        values: { name: '${{ parameters.name }}', owner: '${{ parameters.owner }}' }
    - id: publish
      name: Create repository
      action: publish:github
      input:
        repoUrl: github.com?owner=acme&repo=${{ parameters.name }}
        defaultBranch: main
    - id: register
      name: Register in catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

Where Backstage fits among portal options — it’s the default, not the only choice:

Portal option Type Pros Cons
Backstage Open-source framework (CNCF) Hugely extensible, big plugin ecosystem, vendor-neutral You build and run it; non-trivial to operate
Port SaaS IDP portal Fast to stand up, low ops Cost; less code-level control
Cortex SaaS (catalog + scorecards) Strong service maturity scorecards Cost; opinionated
OpsLevel SaaS (catalog + standards) Good for service ownership/standards Cost
Cloud-native (Crossplane + CLI/GitOps) Build-your-own control plane Declarative, K8s-native, no portal to run More plumbing; no UI unless you add one
Roll-your-own portal Internal app Exactly your needs Highest build/maintenance cost

A blunt caution: Backstage is not your platform strategy. Standing up a portal without golden paths behind it gives you a pretty catalog of services and no removed toil. Build the paved roads first; the portal is how developers discover and trigger them.

Team Topologies: the operating model that makes it work

The reason most platform efforts fail is organisational, not technical — so the org model deserves equal billing. Team Topologies (Skelton & Pais) gives the language. It defines four team types and three interaction modes, and a platform team only works if it sits in the right relationship to the rest.

The four team types:

Team type Purpose Example Relationship to the platform
Stream-aligned Owns a slice of business value end to end The payments team, the checkout team The platform’s customers
Platform Provides self-service internal products that reduce others’ cognitive load The IDP / golden-path team Runs the platform as a product
Enabling Helps stream-aligned teams adopt new skills/practices, then leaves An SRE-coaching team teaching SLOs Boosts adoption; not a permanent dependency
Complicated-subsystem Owns a part needing deep specialist expertise A real-time pricing engine, ML platform May consume or be consumed by the platform

The three interaction modes, and which the platform should default to:

Interaction mode What it means When the platform uses it
X-as-a-Service Consume a capability with minimal collaboration The default — golden paths are self-service
Collaboration Two teams work closely for a time to figure something out Early, when co-designing a new golden path with a pilot team
Facilitating One team helps another improve (enabling-style) When helping a team adopt the platform or unblock a gap

The decisive principle: a platform team should interact with stream-aligned teams primarily as X-as-a-Service. If the dominant mode is collaboration (the platform team has to get involved every time a product team ships) you haven’t built a platform — you’ve built a bottleneck wearing a platform’s name badge. The platform’s whole value is that product teams don’t need to talk to it to get the common things done.

How a platform team avoids becoming the bottleneck it was meant to remove — these are the practices, stated as rules:

Architecture at a glance

Two diagrams tell the whole story: how the four metrics are derived (the scoreboard) and how the platform delivers golden paths (the engine that moves the scoreboard).

The first diagram traces DORA measurement end to end. Read it left to right: developer commits flow into version control, which timestamps each commit (the start of lead time); changes move through CI (build, test, scan — where quality gates catch failures before production, protecting change failure rate) and into CD, whose successful production deploy is the event that feeds deployment frequency and stops the lead-time clock. In production, observability and SLOs watch the running service; when a deploy degrades the service, an incident opens (its start-to-restore span is time to restore, and its link back to the deploy is change failure rate). Every one of these events streams into a metrics store, and a dashboard renders the four numbers and their trend. The shared insight the diagram encodes: the metrics are not opinions, they are computed from the events your delivery system already produces — commit times, deploy events, incident spans.

DORA metrics measurement pipeline: developer commits enter version control which timestamps them as the start of lead time, flow through CI build-test-scan quality gates and CD into a successful production deployment that feeds deployment frequency, while production observability and SLOs detect deploy-caused incidents whose start-to-restore span feeds time to restore and whose link to the deploy feeds change failure rate, with every event streaming into a metrics store and rendered as the four DORA numbers on a dashboard

The second diagram shows the platform engineering engine that those metrics drive investment into. A stream-aligned team enters through the developer control plane — a Backstage portal, a CLI, or a Git manifest — and invokes a golden path (create a service, get a database, ship a change). That intent flows into the integration and delivery plane (CI/CD templates, GitOps controllers, the image registry, the secrets operator), which realises it against the resource plane (Kubernetes, managed databases, queues, cloud accounts), with the security plane (policy-as-code, scanning, RBAC) enforced across every path and the observability plane wiring default dashboards and SLOs. The output is a fully scaffolded, secured, observed service the team owns — produced in minutes, not weeks. The footer insight: the platform team operates this as X-as-a-Service, so the stream-aligned team self-serves without a ticket, and the DORA metrics from the first diagram are how the platform team proves the engine is working.

Internal Developer Platform delivering golden paths: a stream-aligned team enters through the developer control plane of portal, CLI or Git manifest and invokes a golden path such as create-a-service, which flows into the integration and delivery plane of CI/CD templates, GitOps controllers, image registry and secrets operator, is realised against the resource plane of Kubernetes, managed databases, queues and cloud accounts, with a security plane of policy-as-code, scanning and RBAC enforced across every path and an observability plane wiring default dashboards and SLOs, producing a fully scaffolded secured observed service the team owns in minutes, operated by the platform team as X-as-a-Service

Real-world scenario

Meridian Logistics runs a B2B shipping platform: about 38 engineering teams, ~140 microservices, mostly on Kubernetes across two regions, with a four-person nascent “DevOps tools” group. The CTO had just been asked by the board whether a year of “DevOps transformation” had paid off, and had no number to give. Onboarding a new service took two to three weeks; the variance between teams was enormous — one team deployed twelve times a day, another deployed once every six weeks and treated each release as a controlled detonation.

Phase 1 — measure (weeks 1–6). The tools group resisted the urge to build a platform and instead instrumented DORA. They created a deployments table and added a single record-deployment step to the shared deploy job most teams already used; for the holdout teams they backfilled from the GitHub Deployments API. They linked their incident tool (Opsgenie) to deployments by service-and-version, and computed all four metrics per team with the SQL above, rendered in Grafana. The picture was stark: organisation-wide median lead time was 9 days, deployment frequency was bimodal (a cluster of elite teams and a long tail of monthly deployers), change failure rate averaged 23%, and time to restore had a brutal p90 of 6 hours driven by poor observability on the slow-moving teams. Critically, the decomposition of lead time showed the dominant cost was not CI (median 14 minutes) but review wait (median 2.1 days) and a manual change-approval board that met twice a week (median 3 days waiting for the board). The board now had its number — and the tools group had its target.

Phase 2 — engineer the bottleneck (months 2–7). The data said the biggest lever was the change-approval board and review latency, plus the long tail of teams with bespoke, fragile pipelines. So the platform team built a golden path: a Backstage software template that scaffolded a new service with a standard pipeline, default dashboards, an SLO stub, secrets wiring and a catalog-info.yaml; a set of reusable CD templates with canary deployment and automated rollback baked in (which made change failures self-recording); and they worked with the security and risk teams to replace the change-approval board with automated policy gates in the golden-path pipeline (tests, scans, and a canary that auto-rolls-back on SLO breach were the approval). They ran the first three pilots as collaboration, then flipped to X-as-a-Service for everyone else. They explicitly kept the path optional and documented escape hatches, and tracked adoption as the platform’s headline KPI.

Phase 3 — prove it (months 8–10). Adoption hit 70% of services on the golden path within a quarter. The DORA numbers moved exactly where the data had predicted: median lead time fell from 9 days to 7 hours (eliminating the approval board and shrinking review wait via smaller PRs encouraged by the template), deployment frequency for the long-tail teams rose from monthly to several times a week, change failure rate dropped from 23% to 11% (canary + automated rollback caught bad changes before broad impact), and time to restore p90 fell from 6 hours to 22 minutes (default observability + one-click/automated rollback). New-service onboarding went from ~3 weeks to ~90 minutes. The platform team’s existence was justified not by a slide but by a before/after on four instrumented metrics. The lesson the CTO repeated: “We didn’t get faster by telling teams to go faster. We measured where the time was actually going, then built the platform that gave it back.”

The transformation as a before/after, because the deltas are the story:

Metric Before (org median) After (org median) What moved it
Lead time for changes 9 days 7 hours Killed the approval board; smaller PRs via templates
Deployment frequency (long-tail teams) ~monthly several/week Golden-path CD; one-click deploy
Change failure rate 23% 11% Canary + automated rollback; quality gates
Time to restore (p90) 6 hours 22 minutes Default observability; instant rollback
New-service onboarding ~3 weeks ~90 minutes Scaffolder golden path
Platform adoption 0% 70% in a quarter Run as a product; optional, easiest path

Advantages and disadvantages

The DORA-plus-platform approach is powerful and has real failure modes. Weigh both honestly:

Advantages Disadvantages / risks
Four metrics that correlate with outcomes and resist gaming as a set The metrics can be gamed or weaponised if used per-individual
Localises the bottleneck (lead-time decomposition points at the real cost) Honest measurement needs instrumentation effort and clean event data
A shared definition of “fast and safe” leadership and engineers both trust Mis-defined endpoints (ticket→merge) produce flattering, useless numbers
Platform makes the safe path the easy path — speed and stability rise together A platform built before measuring pain becomes shelfware
Cognitive-load reduction frees stream-aligned teams to build product A too-thick platform (golden cage) blocks advanced teams
Onboarding collapses from weeks to minutes; consistency by default Run as a gatekeeper/ticket queue, the platform becomes the bottleneck
The platform’s value is provable with the same DORA metrics Platform is an ongoing investment (product, on-call, roadmap) — not a one-off
Standards (security, observability) are baked in, not bolted on Adoption is a product problem; mandates breed shadow platforms

When each matters: DORA metrics matter the moment leadership needs to reason about delivery and you have more than a couple of teams; they matter most when you’re about to invest in tooling or process and need a before/after. Platform engineering matters once the cost of every team re-solving infrastructure exceeds the cost of building and running shared paths — typically past five-to-ten teams. Below that scale, a lightweight set of shared templates and good measurement may be all you need; a full IDP with a portal can be premature.

Hands-on lab

Stand up a minimal but real DORA measurement: a deployments table, a couple of recorded deploys, an incident, and queries that compute all four metrics. Free-tier-friendly — local Postgres in Docker; delete at the end. No cloud account needed.

Step 1 — Run a local Postgres.

docker run -d --name dora-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16
sleep 5   # let it come up
docker exec -i dora-lab psql -U postgres -c "SELECT 'ready';"

Expected: a row containing ready.

Step 2 — Create the schema (deployments, deployment_commits, commits, incidents).

docker exec -i dora-lab psql -U postgres <<'SQL'
CREATE TABLE deployments (
  deployment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  service text, team text, environment text, commit_sha text,
  version text, status text, was_rolled_back boolean DEFAULT false,
  deployed_at timestamptz);
CREATE TABLE commits (sha text PRIMARY KEY, authored_at timestamptz);
CREATE TABLE deployment_commits (deployment_id uuid, commit_sha text);
CREATE TABLE incidents (
  id serial PRIMARY KEY, service text, severity text,
  started_at timestamptz, restored_at timestamptz,
  caused_by_deployment_id uuid);
SELECT 'schema created';
SQL

Step 3 — Seed realistic events (two services, several deploys over a week, one failed deploy with an incident).

docker exec -i dora-lab psql -U postgres <<'SQL'
-- commits (authored times)
INSERT INTO commits VALUES
 ('c1', now() - interval '6 days 2 hours'),
 ('c2', now() - interval '4 days 5 hours'),
 ('c3', now() - interval '2 days 1 hour'),
 ('c4', now() - interval '1 day 3 hours'),
 ('c5', now() - interval '5 hours');
-- deployments (production, mostly succeeded; one rolled back)
WITH d AS (
 INSERT INTO deployments (service,team,environment,commit_sha,version,status,was_rolled_back,deployed_at)
 VALUES
  ('checkout-api','payments','production','c1','1.0.0','succeeded',false, now() - interval '6 days'),
  ('checkout-api','payments','production','c2','1.1.0','succeeded',false, now() - interval '4 days'),
  ('checkout-api','payments','production','c3','1.2.0','succeeded',true,  now() - interval '2 days'),
  ('checkout-api','payments','production','c4','1.2.1','succeeded',false, now() - interval '1 day'),
  ('search-api','discovery','production','c5','3.4.0','succeeded',false, now() - interval '4 hours')
 RETURNING deployment_id, commit_sha)
INSERT INTO deployment_commits SELECT deployment_id, commit_sha FROM d;
-- one incident caused by the rolled-back 1.2.0 deploy
INSERT INTO incidents (service,severity,started_at,restored_at,caused_by_deployment_id)
SELECT 'checkout-api','high', now() - interval '2 days' + interval '6 minutes',
       now() - interval '2 days' + interval '23 minutes', deployment_id
FROM deployments WHERE version='1.2.0';
SELECT 'seeded';
SQL

Step 4 — Deployment frequency (per service, last 30 days).

docker exec -i dora-lab psql -U postgres -c "
SELECT service, count(*) AS deploys,
       round(count(*)::numeric / 30, 2) AS deploys_per_day
FROM deployments
WHERE environment='production' AND status='succeeded'
  AND deployed_at >= now() - interval '30 days'
GROUP BY service ORDER BY deploys DESC;"

Expected: checkout-api with 4 deploys, search-api with 1.

Step 5 — Lead time for changes (median, per service).

docker exec -i dora-lab psql -U postgres -c "
WITH p AS (
 SELECT d.service, EXTRACT(EPOCH FROM (d.deployed_at - c.authored_at))/3600.0 AS lead_h
 FROM deployments d
 JOIN deployment_commits dc ON dc.deployment_id=d.deployment_id
 JOIN commits c ON c.sha=dc.commit_sha
 WHERE d.environment='production' AND d.status='succeeded')
SELECT service, round(percentile_cont(0.5) WITHIN GROUP (ORDER BY lead_h)::numeric,1) AS p50_lead_hours
FROM p GROUP BY service;"

Expected: small positive hour values (each deploy lagged its commit by a few hours to a day).

Step 6 — Change failure rate (per service).

docker exec -i dora-lab psql -U postgres -c "
WITH prod AS (
 SELECT * FROM deployments WHERE environment='production' AND status='succeeded'),
failed AS (
 SELECT DISTINCT p.deployment_id FROM prod p
 LEFT JOIN incidents i ON i.caused_by_deployment_id=p.deployment_id
 WHERE p.was_rolled_back OR i.id IS NOT NULL)
SELECT p.service, count(*) total,
       count(f.deployment_id) failed,
       round(100.0*count(f.deployment_id)/count(*),1) cfr_pct
FROM prod p LEFT JOIN failed f ON f.deployment_id=p.deployment_id
GROUP BY p.service ORDER BY cfr_pct DESC;"

Expected: checkout-api 4 deploys, 1 failed, 25% CFR; search-api 0% CFR.

Step 7 — Time to restore (median minutes).

docker exec -i dora-lab psql -U postgres -c "
SELECT service, count(*) incidents,
       round(percentile_cont(0.5) WITHIN GROUP (
         ORDER BY EXTRACT(EPOCH FROM (restored_at-started_at))/60.0)::numeric,1) p50_restore_min
FROM incidents WHERE restored_at IS NOT NULL
GROUP BY service;"

Expected: checkout-api, 1 incident, 17.0 minutes restore.

Validation checklist. You created the event tables, recorded real deploy/commit/incident events, and computed all four DORA metrics with the exact production queries — proving the metrics are nothing more than aggregations over events your delivery system emits. What each step demonstrated:

Step What you did What it proves
2–3 Modeled deploys, commits, incidents The four metrics need only these event sources
4 Counted prod deploys Deployment frequency = a filtered count
5 Joined commit→deploy times Lead time = a duration between two real events
6 Linked rollbacks/incidents to deploys CFR = failed/total, with honest attribution
7 Spanned incident start→restore MTTR = a percentile over incident durations

Teardown.

docker rm -f dora-lab

Cost note. Entirely local; the only “cost” is a Docker container you removed. In production the same schema lives in a managed Postgres (a few hundred rupees a month) or your existing data warehouse.

Common mistakes & troubleshooting

The failure modes that quietly poison a DORA-plus-platform program, as a symptom→cause→confirm→fix playbook:

# Symptom Root cause Confirm Fix
1 Lead time looks “elite” but delivery feels slow Clock stops at merge, not prod Check the end event in your query/tool Re-measure to first production deploy
2 Lead time absurdly long, mostly “waiting” Clock starts at ticket created Inspect the start event Start at first commit of the change
3 Deployment frequency suspiciously high Counting CI runs / staging deploys Filter your event source by environment Count only successful production deploys
4 Change failure rate near 0%, but incidents happen “Failure” defined too narrowly or not attributed Audit how failures are linked to deploys Define user-impacting failure; link incidents→deploys
5 MTTR looks great, users say outages drag on Clock starts at ticket, not impact Compare started_at to first SLO breach Start MTTR at detection/impact, not ticket
6 Metrics get gamed; trust collapses Used to rank individuals Ask how the numbers are reported up Scope to team/service only; never per-person
7 Platform built, nobody uses it Built before measuring pain / user research Check adoption %; ask teams what they needed Build from the worst DORA metric + user research
8 Self-service is really a ticket queue Platform run as ops/gatekeeper Map the request→fulfil path for a new service Provide self-service APIs/templates; product model
9 Advanced teams route around the platform Golden cage; no escape hatches Count services off the path and why Make paths optional; document escape hatches
10 Golden path scaffolds outdated practice Templates not versioned/maintained Diff a year-old scaffold vs current best practice Version templates; provide an update path
11 Platform team is in every team’s deploy Dominant mode is collaboration, not XaaS Observe how often platform is pulled in to ship Move to X-as-a-Service; remove the dependency
12 Numbers improve but users no happier Optimised throughput, ignored reliability Check SLO attainment alongside the four Add the reliability/SLO guardrail metric
13 CFR rises right after “deploy more” push Frequency up without tests/canary Correlate CFR trend with frequency change Add quality gates + canary/automated rollback
14 Metrics differ wildly between two tools Two tools, two definitions of “deploy”/“failure” Compare each tool’s event definitions Standardise on one event source + agreed defs

The three that waste the most time, expanded:

1 & 2 — wrong lead-time endpoints. This is the single most common error and it always flatters or maligns you. The fix is non-negotiable: clock starts at the first commit of the change, stops at that change running in production. If a tool won’t let you set those endpoints, compute it yourself from VCS + the deployments table. Then decompose it — the aggregate is useless for action; the stage breakdown (review wait vs CI vs deploy queue) is where the lever is.

4 — change failure rate that lies. If CFR is near zero while real incidents occur, you either defined “failure” too narrowly or you’re not attributing incidents to deploys. Agree upfront, in writing, that a failure is any user-impacting degradation requiring remediation (rollback, hotfix, flag-off, incident), then link incidents to the deploy that caused them — ideally via an explicit caused_by_deployment_id set during the post-incident step, or automatically via canary rollback.

7 & 8 — the platform nobody wanted. A platform built from the team’s own ideas of “what would be cool,” before measuring where teams actually hurt, becomes shelfware; one run as an approval/ticket function becomes a bottleneck. The cure for both is the same operating-model shift: build from the measured worst metric plus real user research, and ship self-service capabilities run as a product, defaulting to X-as-a-Service.

Best practices

Security notes

Cost & sizing

What drives the cost of this program, and how to size it sanely:

A rough sizing picture:

Investment What you pay for Rough scale What it buys
DORA measurement (self-built) Managed DB + Grafana ~₹500–2,000/month An honest, automated scoreboard
DORA (commercial) Per-seat SaaS per developer/month Fast setup + insights, no build
Platform team 3–8 engineers (product team) salaried, ongoing Golden paths across dozens of teams
Backstage (self-run) Engineering time to run/customise ~0.5–2 FTE ongoing A developer portal you fully control
SaaS IDP portal Subscription per-seat/tier Portal with minimal ops
Observability defaults Logs/metrics/traces ingestion per-GB / per-host Lower MTTR by default

The framing for leadership: the cost is a measurement stack plus a product team; the return is a provable improvement across all four DORA metrics and weeks-to-minutes onboarding — and the same metrics are how you demonstrate the return. Don’t fund the platform without funding the measurement that proves it worked.

Interview & exam questions

1. Name the four DORA metrics and which pair each belongs to. Deployment frequency and lead time for changes are the throughput (velocity) pair; change failure rate and time to restore service are the stability pair. You read all four together — the throughput pair tells you how fast/much, the stability pair how safe/resilient — because any single one in isolation can be gamed.

2. From what events do you measure lead time for changes, and what are the two most common mistakes? From first commit of the change to that change running in production. The two classic errors: starting at “ticket created” (which measures backlog age, not delivery) and stopping at “merged to main” (which hides the merge-to-prod gap — deploy queues, approval boards, release trains). Both must sit on the delivery pipeline.

3. Why is high deployment frequency meaningless on its own? Because frequency is a proxy for batch size, but a team could deploy often and recklessly. Without checking change failure rate and time to restore, a 10×/day cadence with a 40% failure rate looks “elite” but is a fire. The metrics resist gaming only as a set.

4. How do you define a “failure” for change failure rate, and how do you attribute it? A failure is any user-impacting degradation requiring unplanned remediation — rollback, hotfix, feature-flag-off, or an incident — not a test failing in CI (that’s the system working). Attribute it by linking the incident to the deploy: best via an explicit caused_by_deployment_id or an automated canary rollback (which self-records), and least reliably via a time-window heuristic.

5. Where does the clock start for time to restore service, and why does it matter? At impact/detection — when users were first affected (often the first SLO breach), not when a ticket was created. Starting late hides detection time, your worst gap. The clock stops at restored (users served correctly again), which a rollback or failover can achieve before the root-cause resolution.

6. What is platform engineering, and how does it relate to DORA metrics? It’s building an internal product (an IDP with golden paths) that reduces developers’ cognitive load and removes shared toil, making the fast/safe path the default across many teams. DORA metrics tell you which bottleneck to attack and whether the platform worked — you build the platform capability that moves the worst metric, then prove it with the before/after.

7. What is a golden path (paved road), and what must be true for it to succeed? A well-supported, secure-by-default, self-service route for a common task (create a service, ship it, get a DB). It succeeds only if it’s genuinely the easiest option, bakes in standards by default, stays current (versioned templates), and provides escape hatches — it’s an opt-in default, never a mandate, or advanced teams route around it.

8. In Team Topologies, what are the four team types and which interaction mode should a platform team default to? Stream-aligned, platform, enabling, complicated-subsystem. A platform team should default to X-as-a-Service — stream-aligned teams self-serve without needing to collaborate. If the dominant mode is collaboration (the platform team is pulled into every release), you’ve built a bottleneck, not a platform.

9. What are Backstage’s three core features, and is Backstage “the platform”? Software Catalog (registry of services/owners), Software Templates/Scaffolder (golden-path service creation), and TechDocs (docs-as-code in the portal). Backstage is the developer control plane — the front door — not the platform itself; without golden paths behind it you have a catalog and no removed toil.

10. Why should DORA metrics never be measured per individual? Because they describe the delivery system, not a person, and using them to rank individuals causes immediate gaming (people optimise the metric, not the outcome) and erodes trust. They are valid and honest only at the team/service level over time.

11. What is the commonly cited “fifth” DORA metric, and why add guardrails? Reliability (how well the service meets its SLOs / user expectations). You add it and other guardrails (defect-escape rate, batch size) so that over-optimising one of the four — e.g. pushing deployment frequency — doesn’t silently wreck another (e.g. spike the change failure rate) or leave users no happier.

12. How does progressive delivery let you raise deployment frequency without raising risk? By decoupling deploy from release: you deploy the code to production frequently (the metric you want high) but gate the actual exposure to users behind feature flags or a canary (the risk you want controlled). You ship continuously while releasing carefully — and an automated canary rollback even self-records change failures.

These map to platform-engineer and DevOps/SRE interview tracks and to the practices behind the DORA / Accelerate body of knowledge: the four metrics and their definitions test delivery-performance measurement literacy; lead-time endpoints and decomposition test value-stream mapping; CFR definition and attribution test incident-to-deploy linking; platform engineering and golden paths test IDP design; Team Topologies and interaction modes test org design for flow; and progressive delivery with guardrails tests the decouple-deploy-from-release discipline. Engineering-leadership interviews lean on the org-design and operating-model questions (6, 8, 11); senior IC interviews lean on the measurement and attribution questions (1–5, 12).

Quick check

  1. A team proudly reports “lead time: 25 minutes.” What single follow-up question most likely deflates it, and what’s the correct end event?
  2. Deployment frequency is 12×/day and everyone’s thrilled. Which two metrics must you check before calling the team elite, and why?
  3. A deploy goes out, an SLO breaches six minutes later, an engineer rolls back fourteen minutes after that, and files the post-mortem two days later. What is the team’s time-to-restore for this incident, and which timestamp is the wrong one to use as the start?
  4. Your platform team finds itself pulled into a video call every time any product team wants to ship a new service. In Team Topologies terms, what has gone wrong and what mode should it be?
  5. True or false: the right fix for a rising change failure rate after a “deploy more often” initiative is to deploy less often. Explain.

Answers

  1. To production, or to merge?” — a 25-minute lead time almost always stops the clock at merge to main, hiding the merge-to-prod gap (deploy queues, approval boards, release trains). The correct end event is the change running in production; re-measured, it’s usually far longer, and decomposing it reveals where the real time goes.
  2. Change failure rate and time to restore service — the stability pair. Twelve deploys a day with a high failure rate or slow recovery is reckless, not elite; throughput is only impressive alongside good stability. DORA’s metrics resist gaming only as a set.
  3. Restore time ≈ 20 minutes (impact at +6 min from deploy, restored by the rollback at +20 min). The wrong start is when the post-mortem was filed (or when a ticket was created); the clock starts at impact/detection (the SLO breach), and stops at restored (the rollback), not at root-cause resolution two days later.
  4. The platform’s dominant interaction mode has become Collaboration when it should be X-as-a-Service — meaning it’s a bottleneck, not a platform. The fix is self-service golden paths (template + automated pipeline) so product teams ship without talking to the platform team.
  5. False. Deploying less often trades one problem (instability) for another (low throughput) and treats the symptom. The real fix is to keep frequency high and add the safety the push skipped: quality gates, automated tests, and canary/blue-green with automated rollback so frequent deploys are also safe. Speed and stability rise together with the right practices.

Glossary

Next steps

You can now measure delivery with four honest numbers and build the platform that moves them. Build outward:

DevOpsDORAPlatform EngineeringGolden PathBackstageInternal Developer PlatformTeam TopologiesSLO
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading