Last updated: September 10, 2026
What Are DORA Metrics and How Do You Measure Them?
DORA metrics are the five measures that Google's DevOps Research and Assessment program uses to describe software delivery performance: change lead time, deployment frequency, failed deployment recovery time, change fail rate, and deployment rework rate.
They're grouped into throughput and instability so that a gain in one can't quietly come out of the other. Ship twice as often by skipping tests and your change fail rate moves; hold change fail rate at zero by shipping nothing and your deployment frequency collapses. Read on its own, any single number is easy to fake.
This article covers the current definitions, the two changes most dashboards still get wrong, and how to compute each metric from telemetry your CI/CD system and your services already produce.
The five metrics
DORA splits the five across those two factors like this:
| Metric | Factor | What it measures |
|---|---|---|
| Change lead time | Throughput | Time from a commit landing in version control to that commit running in production |
| Deployment frequency | Throughput | How often you deploy to production, or the time between deployments |
| Failed deployment recovery time | Throughput | Time to recover from a deployment that failed and needed immediate intervention |
| Change fail rate | Instability | Share of deployments that needed immediate intervention, usually a rollback or hotfix |
| Deployment rework rate | Instability | Share of deployments that were unplanned work in response to a production incident |
These come from DORA's own definitions, which are worth reading directly, since the wording has shifted more than most dashboards and job descriptions have caught up with.
What changed, and why your dashboard is probably wrong
Open a typical DORA dashboard and you'll see four tiles, one of them labeled MTTR, for mean time to recover. The count is wrong and so is that label.
Start with the label. DORA renamed mean time to recover to failed deployment recovery time in 2023, because the old definition lumped together failures caused by a change and failures caused by something external like a data center outage. Only the first kind tells you anything about your delivery process.
Then in 2024, two things happened at once. Deployment rework rate joined as a fifth metric, and failed deployment recovery time moved out of stability and into throughput. That second move throws people, since recovery time spent a decade as the counterweight to speed. The reasoning holds up once you look at the shape of the numbers: instability is expressed as ratios of deployments that went badly, while throughput covers counts and durations of changes reaching production. A recovery is a change reaching production, just under maximum pressure.
Reliability was never the fifth metric, despite the 2021 report calling it that. DORA has since corrected itself in its history of the metrics: reliability describes operational performance, not delivery performance.
Where the data actually comes from
None of the five can be computed from application telemetry alone. Each one needs a record of when a specific version of a specific service reached production, and your runtime traces don't contain that. The data lives in three systems: your version control system (VCS) holds commit timestamps and review durations, your CI/CD pipeline holds deploy timestamps and outcomes, and your production telemetry and incident records tell you whether the deploy held.
The join key across those three is the part teams skip, and it's why most DORA
dashboards stop at deployment frequency. Get the commit SHA and
service.version onto both the deploy record and the running service, and the
other four metrics turn into arithmetic.
Emit a deployment event
Start with a deployment marker: one event per deployment, sent after the
deployment finishes, whether or not it worked. In Dash0 that's a
dash0.deployment
log event,
and the CLI sends one directly:
12345678dash0 logs send "Deployed checkout-api" \--event-name dash0.deployment \--severity-number 9 \--resource-attribute service.name=checkout-api \--resource-attribute service.version=1.14.2 \--resource-attribute deployment.environment.name=production \--log-attribute deployment.status=succeeded \--log-attribute vcs.ref.head.revision="$GITHUB_SHA"
On success it prints a single line:
1Log record sent
That command needs a static auth_* token. Dash0's OTLP ingress rejects the
OAuth access token that dash0 login writes to your profile, so either pass
--auth-token auth_<...> or set DASH0_AUTH_TOKEN in the environment. This is
the first thing to check if the send fails. The $GITHUB_SHA reference also
assumes you're running inside a CI job that sets it; from a local shell, pass the
commit SHA yourself.

Two of those attributes carry the weight. deployment.status is what makes
change fail rate computable, so send failed from a step that runs only on
failure rather than skipping the event. vcs.ref.head.revision is the join key
back to the commit, which is what makes change lead time computable.
From GitHub Actions, a composite action wraps the same call:
123456789101112131415161718192021222324jobs:deploy:runs-on: ubuntu-lateststeps:- name: Deploy to productionid: deployrun: ./scripts/deploy.sh- name: Send deployment eventif: always()uses: dash0hq/dash0-cli/.github/actions/send-log-event@mainwith:otlp-url: ${{ vars.DASH0_OTLP_URL }}auth-token: ${{ secrets.DASH0_AUTH_TOKEN }}event-name: dash0.deploymentbody: "Deployed checkout-api"severity-number: "9"service-name: checkout-apiservice-version: ${{ github.sha }}deployment-environment-name: productiondeployment-status: ${{ steps.deploy.outcome == 'success' && 'succeeded' || 'failed' }}vcs-repository-url: ${{ github.server_url }}/${{ github.repository }}vcs-ref-head-revision: ${{ github.sha }}vcs-ref-head-name: ${{ github.ref_name }}
The if: always() matters more than it looks. Without it, failed deploys never
report and your change fail rate reads zero forever.
Add pipeline and repository telemetry
OpenTelemetry has had
CI/CD semantic conventions
since semconv 1.27.0, and they've been filled out considerably since.
cicd.pipeline.run.duration is a histogram carrying cicd.pipeline.name,
cicd.pipeline.run.state, and cicd.pipeline.result, whose well-known values
include success, failure, error, timeout, skip, and cancellation.
For GitHub Actions, dash0hq/otel-cicd-action@v4 exports each workflow run as a
trace following those conventions:
12345678910111213jobs:# ... the deploy job from aboveotel-export:if: always()needs: [deploy]runs-on: ubuntu-lateststeps:- uses: dash0hq/otel-cicd-action@v4with:otlpEndpoint: grpc://ingress.eu-west-1.aws.dash0.com:4317otlpHeaders: ${{ secrets.OTLP_HEADERS }}githubToken: ${{ secrets.GITHUB_TOKEN }}
The export has to be the last job to finish, which is what needs: [deploy] and
if: always() buy you. Run it earlier and the trace is incomplete.
The repository side is worth wiring up too. The Collector's
github receiver
scrapes VCS metrics such as vcs.change.time_to_approval and
vcs.change.time_to_merge, which the maintainers describe as leading indicators
for DORA. They decompose lead time for you: if lead time is four days and time
to merge is three and a half, your constraint is the review queue, not the
pipeline.
1234567891011121314151617181920212223242526272829extensions:bearertokenauth/github:token: ${env:GH_PAT}bearertokenauth/dash0:scheme: Bearertoken: ${env:DASH0_AUTH_TOKEN}receivers:github:collection_interval: 300sscrapers:scraper:github_org: my-orgsearch_query: "org:my-org"auth:authenticator: bearertokenauth/githubexporters:otlp/dash0:auth:authenticator: bearertokenauth/dash0endpoint: ingress.eu-west-1.aws.dash0.com:4317service:extensions: [bearertokenauth/github, bearertokenauth/dash0]pipelines:metrics:receivers: [github]exporters: [otlp/dash0]
The extensions and service blocks are not optional here. An authenticator
that isn't listed under service.extensions fails to resolve at startup, and
without the pipeline the scraped metrics never leave the Collector. Set
search_query too: the default scrapes every repository in the org, which gets
expensive in API calls once the org is large.
Two caveats on this receiver. It is still an alpha component, and it currently tracks semconv v1.37.0 while the spec is at 1.44.0, so some attribute names lag what the conventions say. Both VCS metrics above are marked as development, so expect the names to move.
Computing the five
With deploy events and pipeline telemetry landing in one place:
- Deployment frequency: count
dash0.deploymentevents per service per window. - Change lead time: for each deploy event, subtract the commit timestamp
behind
vcs.ref.head.revisionfrom the event timestamp. Report the median. A single stale branch merged after three weeks will wreck a mean. - Change fail rate: events with
deployment.status=failed, plus deploys followed by a rollback or hotfix, over all deploys in the window. - Failed deployment recovery time: from a failed deploy event to the deploy event that restored service. Both endpoints are events you already have, which makes this the easiest of the five once markers exist.
- Deployment rework rate: deploys that were unplanned incident response, over all deploys. This one needs a convention your pipeline enforces, such as a label on hotfix branches.
Common pitfalls
The most common measurement bug is counting pipeline runs instead of service
deployments. A monorepo pipeline that deploys nine services registers as one
deploy in a naive dashboard, or as nine when only one service actually changed.
Both are wrong, and the error grows with the repo. Count deploy events keyed on
service.name, not workflow completions.
Dropping failed deployments from the deploy count feels correct, since a failed deploy shipped nothing. It double-counts the good news, though: the failure disappears from throughput and never reaches your instability numbers either. Count every deploy and let change fail rate carry the bad news.
Watch the incident feed too. Piping every production incident into recovery time turns a delivery metric back into an operational one, which is the distinction DORA drew when it renamed MTTR. A certificate expiry at 3 a.m. is a real incident and belongs in your MTTD, MTTA, and MTTR tracking, but it isn't a failed deployment. Filter on deploy-adjacent failures or the metric stops describing your delivery process at all.
Resist rolling the five up into an org-wide score. DORA is explicit that they apply to one application or service at a time, because a mainframe batch job and a React frontend have nothing comparable to say about deployment frequency. Average them and you get a number that moves without meaning anything. And the moment any of the five becomes a target for a team, Goodhart's law handles the rest.
Final thoughts
The five metrics describe how your delivery system behaves. They don't tell you why a particular deploy went badly, and that gap is where the real work sits: a deploy marker on a chart earns its place only if you can pivot from it into the telemetry that changed underneath it.
Dash0 renders dash0.deployment events as
dashboard annotations, so a change fail rate
spike lines up against the release that caused it. From there you get
OpenTelemetry-native distributed traces,
logs, and metrics for the affected
service in one view, with your CI/CD pipeline traces sitting alongside them.
Start a free trial to correlate deployments with your production telemetry. No credit card required
