DORA Metrics: How to Track Deployment Frequency, Lead Time, MTTR, and Change Failure Rate

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

If your team has ever shipped a deployment and immediately wondered whether you’re actually getting faster or just feeling busier — DORA Metrics are the answer. Tracking these four measurements gives any DevOps team a concrete, repeatable way to measure continuous improvement. The hard part isn’t collecting the numbers. It’s figuring out how to collect and act on them without adding busywork to an already full backlog.

DORA (DevOps Research and Assessment) identified four metrics that consistently predict software delivery performance across thousands of organizations:

  • Deployment Frequency — How often code reaches production
  • Lead Time for Changes — Time from first commit to production deployment
  • Mean Time to Recovery (MTTR) — How fast you recover from production failures
  • Change Failure Rate (CFR) — Percentage of deployments that cause incidents

Elite teams deploy multiple times per day, with lead times under an hour and recovery times under an hour, keeping their failure rate below 15%. Most teams land somewhere between “medium” (weekly deploys, days of lead time) and “high” performer. Which approach to measuring these actually makes sense for your team right now? That depends on your size, tooling, and appetite for setup overhead.

Three Approaches to Measuring DORA Metrics

Approach 1: Manual Tracking with Spreadsheets

Lowest friction, zero tooling cost. Your team logs each production deployment, records incident start and end times, and links deployments to commit ranges in a shared spreadsheet. You calculate the four metrics manually each sprint or quarter during a retrospective. No automation required — just consistent discipline from everyone involved.

Approach 2: CI/CD Pipeline Scripts

Instrument your existing pipelines (GitHub Actions, GitLab CI, Jenkins) to emit structured events at each deployment. A deployment success triggers a script that logs the timestamp and commit SHA. A separate incident log — fed by PagerDuty webhooks or manual entries — tracks MTTR. You aggregate with lightweight scripts and optionally push metrics to Grafana or Datadog.

Approach 3: Dedicated DORA Platforms

Tools like Sleuth, LinearB, or Faros AI connect directly to GitHub or GitLab, your deployment tools (Argo CD, Heroku, ECS), and your incident trackers (PagerDuty, OpsGenie). All four DORA metrics get calculated automatically with trend dashboards and no scripting required. Sleuth offers a free tier for small teams; LinearB and Faros AI price per developer seat.

Pros and Cons of Each Approach

Manual Tracking

  • ✅ Zero setup cost, works from day one
  • ✅ No tooling dependency or vendor relationship
  • ❌ Doesn’t scale reliably past five or six people
  • ❌ Data quality depends entirely on team discipline
  • ❌ No real-time visibility — you only see the numbers when someone builds the spreadsheet

CI/CD Pipeline Scripts

  • ✅ Automated, consistent data collection
  • ✅ Full control over what qualifies as a “production deployment”
  • ✅ Integrates with dashboards your team already uses
  • ❌ Requires a few hours of upfront engineering investment
  • ❌ MTTR still needs a separate incident management hook

Dedicated Platforms

  • ✅ Minimal setup, rich dashboards available immediately
  • ✅ Automatically correlates deployments with linked incidents
  • ❌ Vendor lock-in and per-seat cost at scale
  • ❌ Less flexibility for non-standard deployment workflows

Recommended Setup

For teams of 2–10 engineers just getting started: go with the CI/CD pipeline approach. It’s lightweight enough to set up in an afternoon, avoids a new SaaS subscription, and gives you accurate automated data you actually own. Start with Deployment Frequency and Lead Time — they’re the easiest to automate and immediately reveal whether your process is trending in the right direction.

At 15 or more engineers, or if you’re already using PagerDuty or OpsGenie for incident management, evaluate Sleuth (free tier available) or LinearB. The time saved on manually correlating deploys and incidents typically pays for the subscription within the first month.

Implementation Guide

1. Deployment Frequency — Tag Every Production Deploy

Create a timestamped git tag at each production deployment inside your pipeline. Counting those tags over a rolling window gives you deployment frequency with no ambiguity and no extra tooling.

# Add this to your CI/CD deploy job (GitHub Actions, GitLab CI, etc.)
git tag "deploy-prod-$(date +%Y%m%d-%H%M%S)"
git push origin --tags

# Count production deployments in the last 30 days (run locally or in a cron job)
CUTOFF=$(date -d '30 days ago' +%Y%m%d 2>/dev/null || date -v-30d +%Y%m%d)
git tag --list 'deploy-prod-*' | while read tag; do
  tag_date=$(echo "$tag" | grep -oP '\d{8}')
  [[ "$tag_date" -ge "$CUTOFF" ]] && echo "$tag"
done | wc -l

2. Lead Time for Changes — Commit to Production

Lead time measures the gap between when a developer pushes a commit and when it reaches production. This Python script calculates average lead time between two consecutive deploy tags. Pass prev_deploy_tag to scope the commit range precisely; omit it and the script falls back to the last 30 commits on that tag.

import subprocess
import datetime

def calculate_lead_time(deploy_tag: str, prev_deploy_tag: str = None) -> float:
    """Return average lead time in hours for commits in a deployment."""
    deploy_ts = subprocess.run(
        ["git", "log", "-1", "--format=%aI", deploy_tag],
        capture_output=True, text=True
    ).stdout.strip()
    deploy_time = datetime.datetime.fromisoformat(deploy_ts)

    if prev_deploy_tag:
        commit_range = f"{prev_deploy_tag}..{deploy_tag}"
    else:
        commit_range = f"{deploy_tag}~30..{deploy_tag}"

    commits = subprocess.run(
        ["git", "log", commit_range,
         "--first-parent", "--format=%aI"],
        capture_output=True, text=True
    ).stdout.strip().split('\n')

    durations = []
    for ts in commits:
        if not ts:
            continue
        commit_time = datetime.datetime.fromisoformat(ts)
        hours = (deploy_time - commit_time).total_seconds() / 3600
        durations.append(hours)

    return sum(durations) / len(durations) if durations else 0.0

tag = "deploy-prod-20260601-143000"
prev_tag = "deploy-prod-20260531-110000"
print(f"Lead time for {tag}: {calculate_lead_time(tag, prev_tag):.1f} hours")

3. MTTR — Logging and Calculating Recovery Time

Keep a simple JSON log of incidents. Each entry records when the incident started and when it was resolved. Your on-call engineer fills in the resolved_at timestamp after the incident closes.

[
  {
    "id": "INC-042",
    "started_at": "2026-05-20T09:15:00",
    "resolved_at": "2026-05-20T10:02:00",
    "triggered_by_deploy": "deploy-prod-20260520-091000"
  }
]
import json
from datetime import datetime

def calculate_mttr(incidents_file: str) -> float:
    """Return mean time to recovery in minutes."""
    with open(incidents_file) as f:
        incidents = json.load(f)

    times = [
        (datetime.fromisoformat(inc["resolved_at"]) -
         datetime.fromisoformat(inc["started_at"])).total_seconds() / 60
        for inc in incidents
    ]
    return sum(times) / len(times) if times else 0.0

print(f"MTTR: {calculate_mttr('incidents.json'):.0f} minutes")

4. Change Failure Rate — GitHub Actions Integration

Track failed deployments automatically via workflow events. Define “failure” clearly upfront: a deployment that required a rollback, triggered an incident, or prompted a hotfix within one hour counts. Ambiguity here will skew your numbers.

# .github/workflows/record-deploy.yml
name: Record Deployment Metrics

on:
  workflow_run:
    workflows: ["Deploy to Production"]
    types: [completed]

jobs:
  record:
    runs-on: ubuntu-latest
    steps:
      - name: Log deployment outcome
        run: |
          STATUS="${{ github.event.workflow_run.conclusion }}"
          SHA="${{ github.event.workflow_run.head_sha }}"
          TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
          echo "${TIMESTAMP},${STATUS},${SHA}" >> deployments.csv

      - name: Calculate Change Failure Rate
        run: |
          TOTAL=$(wc -l < deployments.csv)
          FAILED=$(grep -c ",failure," deployments.csv || true)
          echo "Total deployments: $TOTAL"
          echo "Failed: $FAILED"
          awk "BEGIN {printf \"Change Failure Rate: %.1f%%\\n\", $FAILED * 100 / $TOTAL}"

Note: deployments.csv is written to the runner’s ephemeral filesystem and will not persist between workflow runs. For reliable historical tracking, upload it as a workflow artifact after each run, or write the data to an external store such as an S3 bucket, a Postgres table, or a GitHub repository file committed back on every run.

Making the Numbers Work for You

Gathering metrics is only half the job — acting on them is where most teams stall. Pick one metric per quarter to actively improve. If your lead time is sitting at three days, that’s your lever. Look at PR review turnaround, test suite duration, and pipeline bottlenecks one at a time rather than trying to fix everything simultaneously.

DORA Metrics don’t tell you what to fix — they tell you where to look. A rising Change Failure Rate after a team expansion points to onboarding and code review process gaps. A stagnant Deployment Frequency during a “we’re moving fast” sprint signals hidden friction in your release pipeline.

Start simple: tag your production deployments today, run the lead time script on your last five releases, and you’ll have two of the four metrics operational by end of week. Add incident logging next sprint. Whatever data you collect in the first 30 days will almost certainly surface one or two obvious improvements you weren’t tracking before — and that’s precisely the point.

Share: