Tracking DORA Cycle Time Metrics from Environment Data
GuideSeptember 29, 202610 min read

Tracking DORA Cycle Time Metrics from Environment Data

The Short Version

Deployment frequency and lead time for changes are two of the four DORA metrics most engineering teams claim to care about and most teams estimate rather than measure, usually by eyeballing ticket timestamps in a project management tool that was never designed to record deploy events precisely. An environment platform's pipeline and event history already contains exact, timestamped records of every deploy, every failure, and every environment's full lifecycle. Computing real DORA numbers from this data, rather than estimating them from proxies, is a matter of a few CLI or API calls, not a new metrics platform.

What the Four Metrics Actually Need as Input

Deployment frequency needs a count of deploy events over a time window. Lead time for changes needs, for each deploy, the gap between when the change was ready (a commit, a PR merge) and when it was actually running. Change failure rate needs the ratio of deploy pipelines that failed to the total that ran. Mean time to restore needs, for each failure, the gap between the failure being detected and a subsequent successful deploy resolving it. All four of these are computable directly from pipeline createdAt and finishedAt timestamps plus pipeline status, which the platform already records for every single deploy.

Deployment Frequency, Computed Directly

Bash
1$ bns pipeline list --environment <ENV_ID> --sort createdAt:desc -o json | \
2    python3 -c "
3import json, sys
4from datetime import datetime, timedelta, timezone
5data = json.load(sys.stdin)
6cutoff = datetime.now(timezone.utc) - timedelta(days=7)
7recent = [p for p in data if datetime.fromisoformat(p['createdAt']) > cutoff]
8print(f'{len(recent)} deploys in the last 7 days ({len(recent)/7:.1f} per day)')
9"
10
1138 deploys in the last 7 days (5.4 per day)

Running this per environment gives a per-service number; running it across every environment in a project gives a team-wide aggregate. Either framing maps directly onto the standard DORA elite/high/medium/low deployment frequency bands, without needing to estimate from a ticket board's "done" column, which usually lags the actual deploy by an inconsistent and untracked amount of time.

Lead Time for Changes, Computed From Pipeline Duration and Commit Timestamp

Lead time needs one more piece of information than deployment frequency: when the underlying change was actually ready, which for a Git-triggered pipeline is the commit timestamp of whatever was deployed. Pairing pipeline createdAt with the triggering commit's authored timestamp gives the full lead time, not just the deploy pipeline's own runtime:

Bash
1$ git log -1 --format=%aI <COMMIT_SHA>
22026-09-29T09:14:22Z
3
4$ bns pipeline show --id <PIPELINE_ID> -o json | \
5    python3 -c "
6import json, sys
7from datetime import datetime
8p = json.load(sys.stdin)
9commit_time = datetime.fromisoformat('2026-09-29T09:14:22+00:00')
10deploy_start = datetime.fromisoformat(p['createdAt'])
11lead_time = (deploy_start - commit_time).total_seconds() / 60
12print(f'Lead time: {lead_time:.1f} minutes from commit to deploy start')
13"
14
15Lead time: 18.3 minutes from commit to deploy start

Aggregating this across a week or month of deploys, rather than looking at any single one, gives the median and p90 lead time DORA reporting actually uses, since a single outlier pipeline (one that waited behind a CI queue for an hour) skews an average badly enough that median is the more honest number to report.

Change Failure Rate, Computed From Pipeline Status

Bash
1$ bns pipeline list --environment <ENV_ID> --sort createdAt:desc -o json | \
2    python3 -c "
3import json, sys
4data = json.load(sys.stdin)
5total = len(data)
6failed = len([p for p in data if p.get('status') == 'failed'])
7print(f'{failed}/{total} deploys failed ({100*failed/total:.1f}%)')
8"
9
103/38 deploys failed (7.9%)

This number counts pipeline-level failures, meaning the deploy itself did not complete successfully, which is a real and useful signal on its own. It is worth being explicit that this specific number does not capture a deploy that completed successfully but introduced a behavioral regression only discovered later (a bug shipped clean and found in production the next day); that category needs to be tracked from incident records or rollback events, not from pipeline status alone, and reported alongside this number rather than folded into it.

Mean Time to Restore, Computed From Consecutive Pipeline Outcomes

MTTR asks a different question than the other three: once something breaks, how long until it's fixed. Against pipeline data, this is measurable as the gap between a failed deploy and the next subsequent deploy against the same environment that succeeds:

Bash
1$ bns pipeline list --environment <ENV_ID> --sort createdAt:asc -o json | \
2    python3 -c "
3import json, sys
4from datetime import datetime
5data = json.load(sys.stdin)
6restores = []
7last_failure = None
8for p in data:
9    if p['status'] == 'failed':
10        last_failure = datetime.fromisoformat(p['createdAt'])
11    elif p['status'] == 'success' and last_failure:
12        restores.append((datetime.fromisoformat(p['finishedAt']) - last_failure).total_seconds() / 60)
13        last_failure = None
14if restores:
15    print(f'Mean time to restore: {sum(restores)/len(restores):.1f} minutes across {len(restores)} incidents')
16"
17
18Mean time to restore: 24.6 minutes across 3 incidents

This is a genuinely useful approximation for a failure whose fix is "push a corrected deploy," which covers most CI/CD-level failures. It undercounts an incident whose actual fix required something outside the deploy pipeline entirely (a manual database intervention, a third-party outage waiting itself out), which is worth noting as a caveat rather than a reason to discard the metric; it measures deploy-pipeline MTTR specifically, a meaningful and honestly-labeled subset of the broader incident MTTR a full incident-management process would track.

Turning This Into a Recurring Report

None of these computations need to run continuously. A scheduled weekly job, run once and posted to wherever the team already reviews engineering metrics, gets the full picture without standing up a dedicated always-on metrics service:

YAML
1# Scheduled GitHub Actions workflow, weekly
2on:
3  schedule:
4    - cron: '0 9 * * MON'
5
6jobs:
7  dora-report:
8    runs-on: ubuntu-latest
9    steps:
10      - run: python3 scripts/compute_dora_metrics.py --project <PROJECT_ID> --days 7 | \
11             tee dora-report.txt
12      - run: gh api repos/${{ github.repository }}/issues/comments -f body="$(cat dora-report.txt)"

Posting the same four numbers on the same cadence, week after week, is what makes trend visible: a single week's change failure rate is noisy on its own, but a rolling four-week view showing it consistently rising is a real, actionable signal, and the only way to see it is by actually recording the number regularly rather than checking it once, ad hoc, when someone asks how deploys are going.

Where This Breaks Down

This approach measures what the environment platform actually sees: deploy pipeline events. It does not automatically capture a manual, unauthorized deploy path that bypasses the platform entirely, which for a team fully standardized on one deploy mechanism is a non-issue, and for a team with lingering legacy deploy paths outside the main pipeline is a real gap worth closing before trusting these numbers as complete. It also treats every environment equally by default; a project with both frequently-redeployed PR environments and a rarely-touched long-lived staging environment should decide explicitly whether to report metrics per environment, per environment type, or aggregated, since blending ephemeral PR-environment deploy frequency with a stable primary environment's much lower frequency produces a number that does not represent either well.

A single week's snapshot of these four numbers is a data point. The actual value comes from watching them move over time, which needs the raw computation above running on a schedule and its output stored somewhere a trend is visible, not just printed to a log that scrolls away after the next run.

Python
1import json
2from datetime import datetime
3
4def append_weekly_snapshot(metrics, filepath="dora_history.jsonl"):
5    with open(filepath, "a") as f:
6        f.write(json.dumps({
7            "week_of": datetime.now().strftime("%Y-%m-%d"),
8            "deploy_frequency_per_day": metrics["deploy_frequency"],
9            "lead_time_median_minutes": metrics["lead_time_median"],
10            "change_failure_rate_pct": metrics["change_failure_rate"],
11            "mttr_median_minutes": metrics["mttr_median"],
12        }) + "\n")

A simple append-only file like this, accumulated week over week, is enough to plot four line charts showing trend direction over a quarter, which is a far more useful artifact for an engineering leadership review than any single week's numbers in isolation. A change failure rate that ticked up from 4% to 6% in one week is noise; the same number holding steady at 6% for six straight weeks is a real signal worth investigating, and the only way to tell the two apart is having the historical series to look at.

Common Ways These Numbers Get Misread

Comparing deployment frequency across teams with fundamentally different deploy models. A team practicing trunk-based development with small, frequent deploys will show a much higher deployment frequency than a team doing weekly release trains, and this difference reflects a deliberate process choice, not necessarily one team being more mature or effective than the other. Comparing the two numbers directly, without that context, produces a misleading conclusion.

Treating a single low change-failure-rate week as proof the numbers no longer need watching. A quiet week with few risky changes deployed can show an artificially good change failure rate simply because nothing risky happened to be deployed, not because the team's actual failure-handling has improved. The trend over several weeks matters more than any single week's reading.

Using lead time as an individual performance metric rather than a system health metric. Lead time measures the system a team operates within (how fast CI runs, how fast environments deploy, how much queueing happens), not an individual developer's personal speed. Attributing a slow lead-time week to "the team was slow" rather than "the CI queue was backed up" misdiagnoses the actual cause and points any resulting action at the wrong fix entirely.

Segmenting Metrics by Change Type

A single blended lead-time or change-failure-rate number across every kind of change a team ships (a one-line config tweak, a multi-service feature, a database migration) tends to obscure more than it reveals, since these categories genuinely have different risk profiles and different reasonable expectations. Segmenting the same underlying pipeline data by a label attached at PR-open time (a simple tag: type:config, type:feature, type:migration) turns one blended number into several more actionable ones:

Python
1by_type = {}
2for pipeline in pipelines:
3    change_type = pipeline.get('labels', {}).get('change_type', 'unspecified')
4    by_type.setdefault(change_type, []).append(pipeline)
5
6for change_type, pipelines_of_type in by_type.items():
7    failed = len([p for p in pipelines_of_type if p['status'] == 'failed'])
8    print(f"{change_type}: {failed}/{len(pipelines_of_type)} failed")
Text
1config:    1/22 failed (4.5%)
2feature:   2/14 failed (14.3%)
3migration: 1/4  failed (25.0%)

A blended change failure rate across all three would report something in the middle, obscuring the genuinely useful signal that migrations, specifically, are where extra review scrutiny or additional automated safety checks would have the most impact, since that's where failures are actually concentrating.

For the full API surface this data pulls from beyond the CLI examples shown here, see Automate Everything: Using the Bunnyshell Public API. For the queuing math behind why lead time spikes disproportionately once deploy volume grows, see Staging Environment Capacity Math.

Sharing the Numbers Without Turning Them Into a Weapon

A last, important operational note: these metrics are most useful, and most honestly reported, when the team understands they exist to reveal where the system needs attention (a slow CI queue, an under-tested migration path), not to rank individual engineers against each other. A team that suspects its DORA numbers are being used to evaluate individuals rather than the system tends to, consciously or not, start optimizing for the metric rather than the underlying goal it was meant to represent, batching changes to reduce deploy frequency reporting or avoiding a risky-but-necessary refactor to keep the change failure rate looking good. Framing and communicating these numbers explicitly as system-health indicators, reviewed at the team or org level rather than the individual level, is what keeps them trustworthy enough to actually act on.

Checklist

  • Deployment frequency computed from real pipeline createdAt timestamps, not ticket-board estimates
  • Lead time paired against the triggering commit's actual timestamp, not just pipeline runtime
  • Change failure rate reported alongside (not merged with) any separately-tracked post-deploy regression rate
  • MTTR computed from consecutive failed-then-successful pipeline pairs, with the "deploy-pipeline MTTR" scope stated explicitly
  • Metrics reported on a recurring cadence, not measured once and left stale
✦✦

Compute real DORA metrics from the deploy data you already have.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.

Frequently Asked Questions

What are the four DORA metrics, briefly?

Deployment frequency (how often code ships), lead time for changes (time from commit to running in production or an equivalent environment), change failure rate (the share of deploys that cause a failure requiring remediation), and mean time to restore (how long a failure takes to fix once it happens).

Why use environment platform data instead of a dedicated DORA metrics tool?

A dedicated tool is worth adopting for a full, polished dashboard, but it usually needs to be pointed at the same underlying signal: pipeline timestamps and deploy outcomes. If a team already has this data sitting in its environment platform's API, computing the core numbers directly is a same-day exercise rather than a new tool rollout, and it is useful for validating whatever a dedicated tool reports later.

Is PR-environment deploy time a fair proxy for production lead time?

For teams practicing continuous deployment where merging to main and deploying to production happen close together, yes, closely. For teams with a slower, separate production release process, PR-environment deploy time measures a real and useful number, just a different one: the lead time up to a change being fully validated, not the full lead time to production specifically.

How do you compute change failure rate from pipeline data?

As the share of deploy pipelines with a failed status divided by total deploy pipelines over the same window. This counts pipeline-level deploy failures directly; it does not by itself capture a deploy that succeeded but caused a behavioral regression discovered later, which needs to be tracked separately, typically via incident or rollback records.

Does tracking these metrics require an extra tool running continuously?

No, they can be computed on demand from existing pipeline and event history via the CLI or API, run as a scheduled report rather than a continuously running service, which is enough for a weekly or monthly engineering metrics review.