devops

DORA Metrics: How to Measure Them with GitHub Actions and Prometheus

Instrument the four DORA metrics — deployment frequency, lead time, change failure rate, MTTR — with GitHub Actions, Prometheus, and Grafana. Real configs.

July 17, 2026·8 min read·
#devops#cicd#github-actions#prometheus#grafana#monitoring#sre

The four metrics, in one paragraph

DORA metrics are four numbers that describe how well your team ships software: deployment frequency (how often you deploy to production), lead time for changes (commit to running in production), change failure rate (what fraction of deploys degrade production), and failed deployment recovery time (how fast you recover when one does — formerly called MTTR). The first two measure throughput, the last two measure stability, and the research behind them (Google's DORA program, six years of State of DevOps reports) keeps finding the same thing: good teams are fast and stable, not one at the expense of the other.

The problem is that most teams either don't measure them at all, or buy a dashboard product that shows numbers nobody trusts because nobody knows where they come from. This guide does it the transparent way: your GitHub Actions pipeline emits the raw events, Prometheus stores them, Grafana displays them. Every number is a PromQL query you can read. Total new infrastructure: one Pushgateway container.

The instrumentation model

The trick that makes this simple: every DORA metric is derived from deployment events. A deploy happens (frequency), it carries commits of a certain age (lead time), it either degrades production or it doesn't (failure rate), and if it does, some time passes before the fix lands (recovery time). So instead of scraping the GitHub API and correlating after the fact, we emit one metric bundle from inside the deploy job itself, where all the context — SHA, commit timestamps, outcome — is already sitting in environment variables.

Since CI jobs are short-lived, Prometheus can't scrape them. We push through a Pushgateway:

# docker-compose snippet on your monitoring host
  pushgateway:
    image: prom/pushgateway:v1.9.0
    ports:
      - "9091:9091"

And one scrape job in prometheus.yml:

  - job_name: pushgateway
    honor_labels: true     # keep the labels the CI job pushed
    static_configs:
      - targets: ["pushgateway:9091"]

honor_labels: true matters — without it Prometheus overwrites the job and service labels your pipeline pushed with the scrape target's own, and every service's deploys collapse into one series. If you don't have this stack yet, the Prometheus + Grafana setup guide gets you there in an afternoon.

Deployment frequency and lead time: one workflow step

Add this step to the end of your production deploy job. It computes lead time from the oldest commit in the deployed range (per DORA's definition — commit to production, not merge to production) and pushes both metrics in one shot:

      - name: Emit DORA metrics
        if: success()
        env:
          PUSHGATEWAY: ${{ secrets.PUSHGATEWAY_URL }}
        run: |
          NOW=$(date +%s)
          # Oldest commit deployed in this release. Falls back to HEAD's
          # timestamp on the very first deploy (no previous SHA recorded).
          PREV=$(curl -sf "$PUSHGATEWAY/metrics" \
            | awk '/deployment_sha_info.*service="payments-api"/ {print}' \
            | grep -oP 'sha="\K[a-f0-9]+' || true)
          if [ -n "$PREV" ]; then
            OLDEST=$(git log --format=%ct "$PREV..$GITHUB_SHA" | tail -1)
          else
            OLDEST=$(git log -1 --format=%ct "$GITHUB_SHA")
          fi
          LEAD=$((NOW - ${OLDEST:-$NOW}))

          cat <<EOF | curl -sf --data-binary @- \
            "$PUSHGATEWAY/metrics/job/dora/service/payments-api/env/production"
          # TYPE deployment_timestamp_seconds gauge
          deployment_timestamp_seconds $NOW
          # TYPE deployment_lead_time_seconds gauge
          deployment_lead_time_seconds $LEAD
          # TYPE deployment_sha_info gauge
          deployment_sha_info{sha="$GITHUB_SHA"} 1
          EOF

You need fetch-depth: 0 on your actions/checkout step so git log can see history beyond the tip commit. Everything else is stock. If your pipeline is still a pile of untested YAML, fix that first — the complete GitHub Actions production setup covers the deploy job this step bolts onto.

Now the PromQL. Deployment frequency is just "how many times did the timestamp gauge change":

# Deploys per service over the last 30 days
changes(deployment_timestamp_seconds{env="production"}[30d])

Lead time — Pushgateway holds the latest deploy's value, so average it across the window:

# Average lead time (seconds) over 30 days
avg_over_time(deployment_lead_time_seconds{env="production"}[30d])

One honest caveat: avg_over_time over a gauge is time-weighted, so a deploy that sits as the latest value for a week counts more than one replaced an hour later. For a team deploying regularly the distortion is small; if you need per-deploy medians, record the same events into a histogram via a tiny exporter instead. Start with the gauge — a slightly approximate number your team trusts beats a perfect number nobody has.

Change failure rate: count the rollbacks

DORA defines a failed change as a deploy that degrades service and needs remediation — a rollback, hotfix, or patch. Not a red CI run; a failed build never reached users. The cleanest signal is the remediation itself: if your recovery path is a rollback workflow (or a revert-and-redeploy), have that increment the failure count:

# In your rollback workflow, after the rollback succeeds
      - name: Record failed deployment
        run: |
          FAILED_AT=$(curl -sf "$PUSHGATEWAY/metrics" \
            | awk '/deployment_timestamp_seconds.*service="payments-api"/ {print $2}')
          NOW=$(date +%s)
          cat <<EOF | curl -sf --data-binary @- \
            "$PUSHGATEWAY/metrics/job/dora_failures/service/payments-api/env/production"
          # TYPE deployment_failure_timestamp_seconds gauge
          deployment_failure_timestamp_seconds $NOW
          # TYPE deployment_recovery_seconds gauge
          deployment_recovery_seconds $((NOW - ${FAILED_AT%.*}))
          EOF

Two metrics for the price of one: the failure event (numerator of change failure rate) and the recovery duration (time from the bad deploy landing to the rollback completing — your failed deployment recovery time). If you page on SLO burn instead of eyeballing dashboards, "degrades service" already has a precise definition in your stack: a fast-burn alert on the error budget. That's exactly the error budget machinery doing double duty, and it keeps the failure count honest — no arguing about whether a deploy "really" failed.

The rate:

# Change failure rate over 30 days
changes(deployment_failure_timestamp_seconds{env="production"}[30d])
/
changes(deployment_timestamp_seconds{env="production"}[30d])
# Failed deployment recovery time (avg, seconds)
avg_over_time(deployment_recovery_seconds{env="production"}[30d])

If you deploy through Argo CD rather than push-style pipelines, the same model applies — emit the metrics from a PostSync hook and on argocd app rollback. The Argo CD best-practices guide covers the sync hooks this hangs off.

The Grafana dashboard: four stats, four graphs

One row of stat panels (current 30-day value) and one row of trend graphs (same queries, [7d] window, over 90 days of history). For thresholds, use the tiers from the DORA research — 2024 State of DevOps report values:

MetricEliteHighMediumLow
Deployment frequencyOn demand (multiple/day)Daily–weeklyWeekly–monthlyMonthly or less
Lead time for changes< 1 day1 day–1 week1 week–1 month> 1 month
Change failure rate~5%~10%~15%> 40%
Failed deploy recovery< 1 hour< 1 day1 day–1 week> 1 week

Map those to Grafana's green/yellow/orange/red thresholds and the dashboard answers the only question leadership actually asks: are we getting better or worse? Set the stat panel unit for lead time and recovery to seconds and Grafana renders "4.2 h" instead of a raw integer.

How teams ruin DORA metrics

The metrics are descriptive, not prescriptive, and every failure mode I've seen comes from forgetting that:

  • Making them individual KPIs. Deployment frequency per engineer is meaningless and invites 20 one-line deploys before performance review. These are team and system metrics. Goodhart's law applies in full: the moment a measure becomes a target, people optimize the measure.
  • Comparing teams against each other. A team owning a stateful billing system and a team owning a stateless frontend have structurally different ceilings. Compare each team against its own trend line, nothing else.
  • Gaming the failure definition. If "failed deploy" requires a human to tag it, the rate mysteriously trends to zero. That's why the instrumentation above ties failure to the rollback event — a mechanical signal nobody edits.
  • Measuring throughput without stability. Doubling deploy frequency while change failure rate climbs from 5% to 25% is a regression wearing a velocity costume. The four move as a set; report them as a set.
  • Stopping at DORA. The same researchers followed up with the SPACE framework precisely because four delivery metrics can't see satisfaction, collaboration, or cognitive load. DORA tells you the pipeline is healthy; it says nothing about whether the humans are. Pair the dashboard with a quarterly developer survey before drawing conclusions about "productivity" — this is the throughput-vs-sustainability tension the SRE vs DevOps split exists to manage.

Where the numbers usually point

Once the dashboard has a month of data, the same patterns show up. Lead time dominated by review wait, not pipeline time → the fix is smaller PRs, not faster runners. Deploy frequency stuck at weekly because deploys are scary → invest in zero-downtime deployments so shipping stops being an event. Recovery time measured in hours because rollback is a runbook, not a button → automate the rollback path you just instrumented.

That's the real payoff. The metrics cost you ~40 lines of YAML and a Pushgateway, and in return every improvement argument you make — "we need smaller batches", "we need progressive delivery" — stops being an opinion and starts being a graph.

#devops#cicd#github-actions#prometheus#grafana#monitoring#sre
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →