Dash0 acquires Polar Signals

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:

MetricFactorWhat it measures
Change lead timeThroughputTime from a commit landing in version control to that commit running in production
Deployment frequencyThroughputHow often you deploy to production, or the time between deployments
Failed deployment recovery timeThroughputTime to recover from a deployment that failed and needed immediate intervention
Change fail rateInstabilityShare of deployments that needed immediate intervention, usually a rollback or hotfix
Deployment rework rateInstabilityShare 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:

bash
12345678
dash0 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:

1
Log 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:

yaml
123456789101112131415161718192021222324
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
id: deploy
run: ./scripts/deploy.sh
- name: Send deployment event
if: always()
uses: dash0hq/dash0-cli/.github/actions/send-log-event@main
with:
otlp-url: ${{ vars.DASH0_OTLP_URL }}
auth-token: ${{ secrets.DASH0_AUTH_TOKEN }}
event-name: dash0.deployment
body: "Deployed checkout-api"
severity-number: "9"
service-name: checkout-api
service-version: ${{ github.sha }}
deployment-environment-name: production
deployment-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:

yaml
12345678910111213
jobs:
# ... the deploy job from above
otel-export:
if: always()
needs: [deploy]
runs-on: ubuntu-latest
steps:
- uses: dash0hq/otel-cicd-action@v4
with:
otlpEndpoint: grpc://ingress.eu-west-1.aws.dash0.com:4317
otlpHeaders: ${{ 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.

yaml
1234567891011121314151617181920212223242526272829
extensions:
bearertokenauth/github:
token: ${env:GH_PAT}
bearertokenauth/dash0:
scheme: Bearer
token: ${env:DASH0_AUTH_TOKEN}
receivers:
github:
collection_interval: 300s
scrapers:
scraper:
github_org: my-org
search_query: "org:my-org"
auth:
authenticator: bearertokenauth/github
exporters:
otlp/dash0:
auth:
authenticator: bearertokenauth/dash0
endpoint: ingress.eu-west-1.aws.dash0.com:4317
service:
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.deployment events per service per window.
  • Change lead time: for each deploy event, subtract the commit timestamp behind vcs.ref.head.revision from 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

Authors