Six months from now, somebody at Reservalia will ask: "has any of this been any use?". If the team answers "yes, we go much faster now", the conversation turns into an argument about feelings. If they answer "the time from commit to production has dropped from 9 days to 40 minutes and the percentage of deployments causing an incident has gone from 15% to 3%", the conversation is over. This lesson is about that: turning the quality of software delivery into numbers you can verify. The DORA metrics are the industry standard for doing so, and their greatest virtue is that there are only four of them, they are calculated from data the pipeline itself generates, and they cannot all be gamed at once — improving speed by wrecking stability shows up immediately. By the end you will know what each one measures, which pipeline event the data comes from, how to instrument them without buying any tool, and why turning them into individual targets is the fastest way to destroy their usefulness.
Contents
- What the DORA metrics are and why there are four
- Metric 1: deployment frequency
- Metric 2: lead time for changes
- Metric 3: change failure rate
- Metric 4: time to restore service
- The fifth metric: reliability
- Indicative performance ranges
- How to instrument them simply
- Calculating lead time with bash and json
- Querying the metrics with SQL
- Reservalia's initial dashboard
- Goodhart's law: when the metric becomes the target
- Common mistakes and tips
- Exercises
- Conclusion
- What the DORA metrics are and why there are four
DORA stands for DevOps Research and Assessment, the research programme that has spent years surveying tens of thousands of practitioners to identify which practices distinguish the teams that deliver software better. Its most useful and most quoted contribution is a set of four metrics that summarise delivery performance.
The interesting part is not the list, it is its structure. The four fall into two pairs that pull in opposite directions:
graph TB
subgraph V["⚡ SPEED - how quickly do we deliver?"]
V1["1· Deployment frequency<br/>how often does something reach production?"]
V2["2· Lead time for changes<br/>how long does a change take to arrive?"]
end
subgraph E["🛡️ STABILITY - how well do we deliver?"]
E1["3· Change failure rate<br/>what percentage of changes breaks something?"]
E2["4· Time to restore service<br/>how long do we take to fix it?"]
end
V -.->|"they balance each other"| E
style V fill:#cfe8ff
style E fill:#d9f2d9
This tension is what makes the set hard to fake:
- If you only measured speed, the trivial way to "improve" would be to deploy without testing anything. Stability would collapse and it would show instantly.
- If you only measured stability, the trivial way to "improve" would be never to deploy. Speed would collapse and that would show too.
The programme's central finding is counter-intuitive and worth reading twice: speed and stability are not opposites; they go together. The teams that deploy more often also fail less and recover faster. We saw the reason in lesson 01-02: deploying often forces small batches, and small batches are easier to verify, to diagnose and to revert. Fear of deploying produces large batches, and large batches produce incidents. It is a circle, and it can be travelled in either direction.
- Metric 1: deployment frequency
What it measures. How often an organisation successfully takes code to production.
2.1. How it is calculated
deployment frequency = no. of successful production deployments
────────────────────────────────────────
time periodThree clarifications that change the result a great deal:
- Production only. Deployments to
devandstagingdo not count. What the metric measures is how much value reaches users. - Successful ones only. A deployment that is immediately reverted does not count as delivery; it will count in metric 3.
- Per deployable service, not per company. If Reservalia deploys
apiandwebseparately, each one is measured. Adding up services across the whole organisation produces a big, meaningless number.
2.2. Which pipeline event the data comes from
From the end of the production deployment job, when it finishes correctly. It is the easiest of the four data points to capture: all you need is to record a row every time a deployment to prod finishes green.
2.3. What it really tells you
Deployment frequency is an indirect indicator of batch size, and that is why it matters so much. A team that deploys once a fortnight is accumulating two weeks of changes in every delivery, with everything that entails (section 4 of lesson 01-02).
It also reveals how much friction the process has. Nobody deploys ten times a day if each deployment costs three hours of manual work. A low frequency is almost never a decision: it is a symptom.
- Metric 2: lead time for changes
What it measures. The time that passes from a change being committed to the repository until it is running in production.
It is probably the most valuable of the four, because it cuts across the whole process.
3.1. How it is calculated
It is calculated for each commit and reported as the median (or 50th percentile) for the period. There is an important methodological decision here:
Use the median, not the mean. A single change that sat in a forgotten branch for three months wrecks the mean and represents nothing. The median tells you how long the typical change takes, which is what you want to know. The 90th percentile is useful too: it tells you how bad the usual worst case is.
3.2. Which pipeline event the data comes from
From two events, and that is why it is the metric most people calculate wrongly:
| End | Where the data lives | How to obtain it |
|---|---|---|
| Start | Commit metadata in Git | git show -s --format=%cI <sha> |
| End | The production deployment record | Timestamp of the deployment job |
The usual mistake is measuring from when the pipeline starts instead of from the commit. That measures pipeline performance (useful, but a different thing) and hides precisely what is usually the problem: waiting time. At Reservalia, the pipeline would take 8 minutes, but the change waits up to 9 days for Friday. If you measure from the pipeline start, your lead time is 8 minutes and you would be fooling yourself.
3.3. A nuance about commits and deployments
A deployment usually includes several commits. Lead time is calculated per commit: each of the commits included in the deployment gets the same end date, but has its own start date.
gantt
title Lead time of three commits deployed together
dateFormat YYYY-MM-DD HH:mm
axisFormat %d/%m %H:%M
section Commits
commit a3f9c21 (lead time 26 h) :a1, 2026-03-13 09:00, 26h
commit b7e2d10 (lead time 5 h) :a2, 2026-03-14 06:00, 5h
commit c1d4a55 (lead time 1 h) :a3, 2026-03-14 10:00, 1h
All three are deployed at 11:00 on 14 March, but their lead times are very different. The median of those three would be 5 hours.
3.4. What it really tells you
Lead time breaks the process down. When it is high, the diagnosis consists of asking where the time goes:
| Segment | What lengthens it | Where the course tackles it |
|---|---|---|
| Commit → pipeline started | Nothing, if the trigger is automatic | 02-07 |
| Verification pipeline | A slow suite, no parallelism, no caching | 04-04 |
| Pipeline → review approved | Slow reviews, PRs that are too big | 02-07 |
| Merge → deployment available | No deployment automation | 03-02 |
| Deployment available → production | Deployment windows, slow approvals, fear | 03-01, 03-04 |
In the vast majority of teams, the dominant segment is the last one. And that one is not fixed by buying faster runners.
- Metric 3: change failure rate
What it measures. The percentage of production deployments that cause a degradation of service and require immediate corrective action (rollback, urgent fix, patch).
4.1. How it is calculated
change failure rate = deployments that caused a failure × 100
───────────────────────────────────────
total production deployments4.2. The hard part: defining "failure"
This is the most subjective of the four, and its value depends entirely on the team agreeing a definition before they start measuring and not changing it afterwards. A reasonable operational definition for Reservalia:
A deployment counts as failed if, as a direct consequence, any of these things happens:
- It had to be reverted.
- An urgent fix had to be deployed outside the normal flow in the following hours.
- The service degraded noticeably for users (errors, severe slowness, broken functionality).
- An emergency feature flag had to be activated to switch off what was deployed.
It does not count as failed:
- A failure caught in
stagingthat never reached production. That is the system working. - A bug that had been in production for three months and is discovered now: this deployment did not cause it.
- An outage at an external provider unrelated to the change.
Write that definition down in the repository. Without it, the metric becomes useless as soon as there is an ambiguous case and somebody decides on the spot.
4.3. Which pipeline event the data comes from
This is the only one of the four data points that does not come out of the pipeline automatically: it requires a human decision. The practical sources are:
- An automatic rollback triggered by the metrics: it records itself (03-05).
- A deployment marked manually as failed by whoever is handling the incident.
- The link between the incident and the deployment that caused it, established in the post-mortem.
The most practical option is a command or a button Nuria can use during the incident:
# Mark the last production deployment as failed.
# Run by whoever is handling the incident, while handling it.
./scripts/mark-deployment-failed.sh \
--deployment dep_2026_0314_1042 \
--reason "500 error creating appointments after the schema change" \
--detected-at "2026-03-14T11:05:00+01:00"If recording the failure takes more than thirty seconds, nobody will do it during a crisis and your data will be false.
4.4. What it really tells you
A high change failure rate points to specific gaps in verification: insufficient tests, a staging environment that does not resemble production, or batches that are too large. A suspiciously low value (a sustained 0%) usually means that nothing is being recorded, not that nothing fails.
- Metric 4: time to restore service
What it measures. How long the organisation takes to restore service when there is a degradation or outage in production.
5.1. How it is calculated
It is reported as the median across the period's incidents.
Watch out for the two ends:
- Incident start: when the problem began to affect users, not when somebody noticed. If the service was broken for 25 minutes before a customer called, those 25 minutes count. Measuring from detection rewards having poor monitoring, which is exactly the opposite of the desirable incentive.
- Restoration: when users are fine again, not when the root cause is understood. A rollback restores service even if nobody yet knows what happened. The analysis comes afterwards.
5.2. Which event the data comes from
From incident management: start time and resolution time. Ideally, the start time is provided by automated monitoring (the subject of 03-06) and not by anybody's memory.
5.3. What it really tells you
Along with frequency, it is the metric that improves fastest when you automate. At Reservalia today, restoring means rebuilding and re-uploading over SFTP: around an hour. With immutable artifacts in ECR, restoring means redeploying the previous image: minutes.
This metric captures a deep DevOps idea: you cannot stop failures from happening; you can make them short. A team that fails 5% of the time but recovers in 4 minutes offers a far better service than one that fails 2% of the time and takes 6 hours.
- The fifth metric: reliability
A fifth was later added to the classic four: reliability. It is different in nature from the others, which is why it deserves a separate explanation.
What it measures. The extent to which the service meets its users' expectations in terms of availability, latency and correctness.
The first four measure the delivery process; the fifth measures the operational outcome. It is the one that prevents a tempting reasoning error: a team can have all four metrics in the green and still offer a poor service — slow, with functionality that is correct but unusable, or down outside the moments that count as an "incident".
It has no single formula. It is instrumented through service level objectives (SLOs) defined by the team itself. Three reasonable ones for Reservalia:
| Service level objective | Threshold | Why this one |
|---|---|---|
| API availability | ≥ 99.9% monthly during business hours | An outage at 4 in the morning is less annoying than one at 11:00 on a Saturday |
| Appointment creation latency | 95th percentile < 800 ms | If booking is slow, the end customer gives up |
| Reminder delivery success | ≥ 99.5% sent in the correct window | A reminder that does not arrive is a lost appointment for the business |
Note the nuance in the first one: "during business hours". A booking platform for hair salons and clinics earns its value between 09:00 and 20:00. A flat availability objective ignores that. Defining an SLO well consists precisely of capturing what genuinely matters to the user.
The tools for measuring this — observability, dashboards and alerts — appear in lesson 03-06.
- Indicative performance ranges
The DORA programme classifies teams into four levels. These are the indicative ranges:
| Metric | Low | Medium | High | Elite |
|---|---|---|---|---|
| Deployment frequency | Less than once a month | Between once a month and once a week | Between once a week and once a day | On demand, several times a day |
| Lead time for changes | More than a month | Between a week and a month | Between a day and a week | Less than a day |
| Change failure rate | High (around 40% or more) | Moderate (around 20-30%) | Moderate-low (around 15%) | Low (around 5% or less) |
| Time to restore service | More than a week | Between a day and a week | Less than a day | Less than an hour |
7.1. An essential warning about this table
These ranges are references to orient you, not targets to chase blindly. Four specific reasons:
- The thresholds move. Every edition of the report readjusts the boundaries. What was "elite" a few years ago is "high" today. Chasing a label is chasing a moving target.
- Context changes everything. A team building certified medical software will never deploy several times a day, and that does not make it a bad team: it makes it a team complying with its sector's regulations. Comparing a SaaS with an avionics system makes no sense.
- Comparing teams with these labels is toxic. They are for comparing yourself with yourself over time. The moment they are used for internal rankings between teams, data manipulation begins (section 12).
- "Elite" is not the right target for everyone. For Reservalia, going from deploying once a week to once a day would be an enormous transformation. Setting "several times a day" as an initial goal only produces frustration.
The right question is not "which level are we at?" but "are we doing better than three months ago?".
- How to instrument them simply
You do not need to buy any tool. It is enough to record one event per deployment and another per incident. That gives you three of the four metrics directly, and the fourth with a manual flag.
8.1. The minimal data model
-- Everything needed for the DORA metrics fits in two tables.
CREATE TABLE deployments (
id TEXT PRIMARY KEY, -- dep_2026_0314_1042
service TEXT NOT NULL, -- 'api' | 'web'
environment TEXT NOT NULL, -- 'dev' | 'staging' | 'prod'
commit_sha TEXT NOT NULL,
commit_date TIMESTAMPTZ NOT NULL, -- ← start of the lead time
deployed_at TIMESTAMPTZ NOT NULL, -- ← end of the lead time
result TEXT NOT NULL, -- 'success' | 'pipeline_failure'
failed BOOLEAN NOT NULL DEFAULT false, -- ← change failure rate
failure_reason TEXT,
commits_included INTEGER NOT NULL DEFAULT 1,
pipeline_url TEXT
);
CREATE TABLE incidents (
id TEXT PRIMARY KEY,
service TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL, -- when it began to AFFECT users
restored_at TIMESTAMPTZ, -- NULL while it is still open
deployment_id TEXT REFERENCES deployments(id), -- if a deployment caused it
severity TEXT NOT NULL -- 'critical' | 'high' | 'medium'
);Two design decisions worth understanding:
commit_dateis stored in the table, rather than queried from Git every time. Repositories get rewritten, branches get deleted and Git queries are slow. Storing the value at deployment time freezes it.deployment_idinincidentsis optional. Not every incident is caused by a deployment (a provider can go down). That relationship is precisely what allows the change failure rate to be calculated without contaminating it with unrelated incidents.
8.2. Where each field gets filled in
flowchart LR
A["Deployment job<br/>to prod finishes OK"] -->|"INSERT"| B[("deployments table")]
C["Monitoring detects<br/>degradation"] -->|"INSERT"| D[("incidents table")]
E["Nuria flags the<br/>guilty deployment"] -->|"UPDATE failed=true"| B
F["Service restored"] -->|"UPDATE restored_at"| D
B --> G["📊 Dashboard<br/>4 metrics"]
D --> G
- Calculating lead time with bash and json
Let us look at the concrete mechanism, step by step. First, obtaining the two ends.
9.1. The starting data
#!/usr/bin/env bash
# record-deployment.sh
# Runs as the LAST step of the production deployment job.
set -euo pipefail # -e: abort if anything fails | -u: error on undefined variables
# -o pipefail: a failure inside a pipe does not go unnoticed
SHA="$(git rev-parse HEAD)"
# The COMMIT date, in ISO 8601 format with a time zone.
# %cI = "committer date, strict ISO 8601". It is the start of the lead time.
COMMIT_DATE="$(git show -s --format=%cI "$SHA")"
# The DEPLOYMENT date: right now, in UTC. It is the end of the lead time.
DEPLOY_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Commit : $SHA"
echo "Committed : $COMMIT_DATE"
echo "Deployed : $DEPLOY_DATE"Why ISO 8601 with a time zone. A
2026-03-14 10:22with no zone is ambiguous: Madrid time, UTC, the runner's? With daylight saving changes in the mix, that ambiguity produces negative lead times or ones an hour too long. Always work in UTC internally and convert only when displaying.
9.2. Calculating the difference
# We convert both dates to seconds since the Unix epoch and subtract.
# This wipes out time zone and month-length problems in one go.
COMMIT_EPOCH="$(date -d "$COMMIT_DATE" +%s)"
DEPLOY_EPOCH="$(date -d "$DEPLOY_DATE" +%s)"
LEAD_TIME_SEC=$(( DEPLOY_EPOCH - COMMIT_EPOCH ))
LEAD_TIME_MIN=$(( LEAD_TIME_SEC / 60 ))
LEAD_TIME_H=$(( LEAD_TIME_SEC / 3600 ))
echo "Lead time: ${LEAD_TIME_SEC}s = ${LEAD_TIME_MIN} min = ${LEAD_TIME_H} h"An example with real numbers:
# Commit : 2026-03-13T09:00:00+01:00 → epoch 1773388800
# Deployed: 2026-03-14T11:00:00Z → epoch 1773486000
# Difference: 97200 seconds = 1620 minutes = 27 hours9.3. Building the record in JSON
# We generate the event as JSON. jq guarantees correct escaping:
# building JSON by concatenating quoted strings is a classic source
# of corrupt files as soon as a commit message contains quotes.
jq -n \
--arg id "dep_$(date -u +%Y%m%d_%H%M%S)" \
--arg service "api" \
--arg environment "prod" \
--arg sha "$SHA" \
--arg commit_d "$COMMIT_DATE" \
--arg deploy_d "$DEPLOY_DATE" \
--argjson lead "$LEAD_TIME_SEC" \
--argjson commits "$(git rev-list --count "${PREVIOUS_SHA}..${SHA}")" \
'{
id: $id,
service: $service,
environment: $environment,
commit_sha: $sha,
commit_date: $commit_d,
deployed_at: $deploy_d,
lead_time_seconds: $lead,
commits_included: $commits,
result: "success",
failed: false
}' > deployment.json
cat deployment.jsonResult:
{
"id": "dep_20260314_110000",
"service": "api",
"environment": "prod",
"commit_sha": "a3f9c21e4b7d8f012345678901234567890abcde",
"commit_date": "2026-03-13T09:00:00+01:00",
"deployed_at": "2026-03-14T11:00:00Z",
"lead_time_seconds": 97200,
"commits_included": 3,
"result": "success",
"failed": false
}9.4. Calculating the lead time of every commit in the deployment
Remember section 3.3: a deployment includes several commits and each has its own lead time. This script calculates them all:
#!/usr/bin/env bash
# lead-time-per-commit.sh <previously_deployed_sha> <current_sha>
set -euo pipefail
PREVIOUS_SHA="$1" # what was in production
CURRENT_SHA="$2" # what we have just deployed
NOW_EPOCH="$(date -u +%s)"
# git rev-list lists every commit between the two points.
# --format=%H|%cI gives: hash|ISO date. --no-commit-header avoids extra lines.
git rev-list --format="%H|%cI" --no-commit-header "${PREVIOUS_SHA}..${CURRENT_SHA}" \
| while IFS='|' read -r sha date; do
commit_epoch="$(date -d "$date" +%s)"
lead_min=$(( (NOW_EPOCH - commit_epoch) / 60 ))
printf '%s %s lead_time=%d min\n' "${sha:0:7}" "$date" "$lead_min"
doneExample output:
c1d4a55 2026-03-14T10:00:00+01:00 lead_time=60 min
b7e2d10 2026-03-14T06:00:00+01:00 lead_time=300 min
a3f9c21 2026-03-13T09:00:00+01:00 lead_time=1620 minThree commits, three very different lead times. The median is 300 minutes (5 hours). The mean would be 660 minutes, badly distorted by the old commit: exactly the reason the median is used.
- Querying the metrics with SQL
With the deployments table populated, the metrics are queries of a few lines.
10.1. Deployment frequency
-- Successful production deployments, per week, for the 'api' service.
SELECT
date_trunc('week', deployed_at) AS week,
COUNT(*) AS deployments
FROM deployments
WHERE environment = 'prod'
AND service = 'api'
AND result = 'success'
AND deployed_at >= now() - interval '12 weeks'
GROUP BY week
ORDER BY week; week | deployments
------------+-------------
2026-01-05 | 1
2026-01-12 | 1
2026-01-19 | 0
2026-01-26 | 2 ← an urgent fix on top of the Friday oneThe "1 per week" pattern immediately gives away Reservalia's Friday ritual.
10.2. Lead time (median and 90th percentile)
-- Median and P90 lead time, in hours, per month.
SELECT
date_trunc('month', deployed_at) AS month,
ROUND(
PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (deployed_at - commit_date))
) / 3600.0, 1
) AS lead_time_median_h,
ROUND(
PERCENTILE_CONT(0.9) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (deployed_at - commit_date))
) / 3600.0, 1
) AS lead_time_p90_h,
COUNT(*) AS n
FROM deployments
WHERE environment = 'prod' AND result = 'success'
GROUP BY month
ORDER BY month;An explanation of the query, for anyone who has not seen PERCENTILE_CONT:
deployed_at - commit_dategives an interval;EXTRACT(EPOCH FROM ...)converts it to seconds.PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ...)calculates the median of those seconds.- It is divided by 3600 to turn it into hours.
COUNT(*)is always included: a median calculated over 2 deployments means nothing, and without the count you would not know.
10.3. Change failure rate
-- Percentage of production deployments that caused a failure, per month.
SELECT
date_trunc('month', deployed_at) AS month,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE failed) AS failed_count,
ROUND(100.0 * COUNT(*) FILTER (WHERE failed) / COUNT(*), 1) AS failure_pct
FROM deployments
WHERE environment = 'prod' AND result = 'success'
GROUP BY month
ORDER BY month; month | total | failed_count | failure_pct
------------+-------+--------------+-------------
2026-01-01 | 5 | 1 | 20.0
2026-02-01 | 4 | 0 | 0.0
2026-03-01 | 4 | 1 | 25.0Note the WHERE result = 'success': the denominator is the deployments that reached production. A pipeline that fails before deploying is not a failed change: it is the system protecting you.
10.4. Time to restore service
-- Median restoration time, in minutes, per quarter.
SELECT
date_trunc('quarter', started_at) AS quarter,
COUNT(*) AS incidents,
ROUND(
PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (restored_at - started_at))
) / 60.0, 1
) AS restore_median_min
FROM incidents
WHERE restored_at IS NOT NULL -- only incidents already closed
AND severity IN ('critical', 'high')
GROUP BY quarter
ORDER BY quarter;10.5. All four at a glance
-- Dashboard: the four metrics for the last quarter.
WITH recent AS (
SELECT * FROM deployments
WHERE environment = 'prod' AND result = 'success'
AND deployed_at >= now() - interval '90 days'
)
SELECT
'Deployment frequency' AS metric,
ROUND(COUNT(*) / 12.85, 2) || ' per week' AS value
FROM recent
UNION ALL
SELECT
'Lead time (median)',
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (deployed_at - commit_date))
) / 3600.0, 1) || ' h'
FROM recent
UNION ALL
SELECT
'Change failure rate',
ROUND(100.0 * COUNT(*) FILTER (WHERE failed) / NULLIF(COUNT(*), 0), 1) || ' %'
FROM recent
UNION ALL
SELECT
'Time to restore (median)',
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (restored_at - started_at))
) / 60.0, 1) || ' min'
FROM incidents
WHERE restored_at IS NOT NULL AND started_at >= now() - interval '90 days';The NULLIF(COUNT(*), 0) avoids a division by zero when there are no deployments in the window: a small detail that causes real dashboard errors in quiet months.
- Reservalia's initial dashboard
Marta has spent an afternoon reconstructing last quarter's data from the Git history, the team channel and the incident notes. This is the measured starting point — the most important number in the whole course, because it is what we will compare everything else against:
| Metric | Reservalia today | Indicative level | Where the data comes from |
|---|---|---|---|
| Deployment frequency | 1.1 per week | Medium | 14 production deployments in 13 weeks |
| Lead time for changes (median) | 6.2 days | Medium | Median of 47 commits deployed in the quarter |
| Change failure rate | 14% | High | 2 of 14 deployments caused a serious incident |
| Time to restore service (median) | 68 minutes | High | Median of the 2 serious incidents |
| Reliability (availability during business hours) | 99.4% | Below their objective (99.9%) | Load balancer logs |
And these are the targets the team sets itself for the end of the course:
| Metric | Today | Target | Main lever | Module |
|---|---|---|---|---|
| Deployment frequency | 1.1 / week | ≥ 5 / week | Remove the Friday window; automated deployment | 3 |
| Lead time (median) | 6.2 days | < 4 hours | Eliminate the waiting; fast CI; automatic deployment to staging | 2 and 3 |
| Change failure rate | 14% | < 5% | Automated tests on every PR; small batches; canary | 2 and 3 |
| Time to restore | 68 min | < 10 min | Immutable artifacts; one-command rollback; alerts | 3 |
| Business-hours availability | 99.4% | ≥ 99.9% | Zero-downtime deployment; healthchecks; multiple instances | 3 |
Three observations about these targets worth internalising:
1. None of them chases the "elite" label. Reservalia has not set out to deploy twenty times a day. It has set out to deploy five times a week, which for them is a radical transformation and, above all, achievable. A target the team does not believe is possible does not motivate: it demoralises.
2. The targets belong to the team, not to individuals. It is "Reservalia's lead time", not "Diego's lead time". We will come back to this in a moment.
3. They come with the specific lever attached. A metric with no associated action is a thermometer with no medicine. Each target points to the module where it is worked on.
graph LR
subgraph HOY["📍 Starting point"]
H1["1.1 deployments/week"]
H2["6.2 days of lead time"]
H3["14% failures"]
H4["68 min to restore"]
end
subgraph MOD["🔧 The course"]
M2["Module 2<br/>CI: tests and artifacts"]
M3["Module 3<br/>CD: deployment and rollback"]
M4["Module 4<br/>Optimisation and security"]
end
subgraph META["🎯 Target"]
O1["≥5 deployments/week"]
O2["<4 h of lead time"]
O3["<5% failures"]
O4["<10 min to restore"]
end
HOY --> MOD --> META
style HOY fill:#ffe8e8
style META fill:#e8ffe8
- Goodhart's law: when the metric becomes the target
There is a foolproof way of destroying the value of everything above, and it is so common that it deserves its own section.
Goodhart's law: "When a measure becomes a target, it ceases to be a good measure."
The mechanism is always the same: if somebody's performance review, bonus or prestige depends on a metric, that person will optimise the metric — which is easier than optimising the reality the metric was supposed to represent.
12.1. How each DORA metric gets gamed
| Metric | If it becomes an individual target… | Real consequence |
|---|---|---|
| Deployment frequency | Changes get artificially chopped into ten empty deployments; the same code is deployed twice | The number goes up, no more value reaches the user, and the noise makes diagnosing incidents harder |
| Lead time | Commits are made just before merging to "reset" the clock; work sits locally for weeks without being committed | The figure improves and real delivery time gets worse, because branches live longer |
| Change failure rate | Incidents get reclassified as "scheduled maintenance"; doubtful cases stop being recorded | The data stops being real and the ability to spot problems is lost |
| Time to restore | The service is declared restored before it is; opening the incident is delayed | Users keep suffering and the dashboard is green |
Note the lead time case: it is especially perverse because the manipulation actively makes the process worse. The developer who avoids committing so as not to "start the clock" is doing precisely the opposite of continuous integration.
12.2. Rules for using the metrics without destroying them
- Never at an individual level. They are system metrics, not personal ones. "The Reservalia team's lead time", never "Diego's lead time". Lead time depends on how fast reviews happen, on the approval process, on how long the pipeline takes and on the deployment window: almost none of that is under one person's control.
- Never to compare teams. A team maintaining a critical legacy system will always have worse numbers than one starting a new product, and that says nothing about their competence. Every team compares itself with itself.
- Never tied to pay or performance reviews. It is the shortest route to false data. As soon as the bonus depends on the number, the number stops describing reality.
- Always all four together. That is the structural protection. Chopping deployments up to raise frequency worsens the change failure rate; not committing to lower lead time reduces frequency. Looking at all four, the manipulations give themselves away.
- As a diagnostic tool, not a judgement. The correct use is: "our lead time is 6 days; let us find out where the time goes". The incorrect one is: "our lead time is 6 days; it has to improve this quarter", with no analysis of the cause.
- Look at the trend, not the absolute value. An isolated reading tells you almost nothing. Twelve weeks of data tell you a lot.
12.3. A practical antidote
When you present the metrics, always accompany them with the question "what is stopping us from improving this?" rather than "why is this bad?". The first opens a technical conversation and produces actions; the second produces excuses and, before long, manipulated data.
Common Mistakes and Tips
Mistake 1: measuring lead time from when the pipeline starts. It is the most frequent mistake and the most deceptive, because it produces excellent numbers. Measure from the commit date; otherwise you hide precisely the waiting time, which is usually the main problem.
Mistake 2: using the mean instead of the median. A change that sat in a forgotten branch for two months distorts the mean completely. The median describes the typical case. Add the 90th percentile if you want to know how bad the usual worst case is.
Mistake 3: counting deployments to staging. Only what reaches users counts. It is a tempting way of inflating deployment frequency without any more value reaching anyone.
Mistake 4: not agreeing in writing what a "failure" is. Without an explicit definition in the repository, each person will classify the doubtful cases their own way and the metric will stop being comparable over time. Agree it before you start measuring.
Mistake 5: chasing the "elite" label. The thresholds change every year and depend on context. The only comparison that matters is with yourself three months ago.
Mistake 6: building the dashboard before you have the pipeline. It is tempting to start with the visual part. But with no automated deployments there are no events to record, and you will end up filling in data by hand, which is exactly where false data is born. Record first, visualise afterwards.
Mistake 7: measuring without a baseline. If you start measuring after automating, you will not be able to prove the improvement. Reconstructing the previous quarter from the Git history and the incident notes, as Marta did, is an afternoon of well-invested work.
Tip 1: record the deployment event from your very first pipeline. It is one table and one INSERT at the end of the job. Five minutes of work that, six months later, are worth an entire conversation with management.
Tip 2: store the SHA and the commit date in the artifact itself. Tagging the image with the SHA and adding the date as a metadata label makes the artifact self-describing: you will always be able to tell which version it is and when it was written, even if the branch no longer exists.
Tip 3: review the metrics as a team, once a month, with no management in the room. The goal of the meeting is to identify bottlenecks, not to account for yourselves. As soon as it is perceived as an evaluation, the data starts to degrade.
Tip 4: complement the four with something qualitative. A quarterly question to the team — "on a scale of 1 to 5, how confident do you feel deploying to production?" — captures something none of the four measures and often anticipates problems before they show up in the numbers.
Exercises
Exercise 1: calculate the four metrics
These are the real figures from another company's last month, Citalia. Calculate their four DORA metrics and classify them indicatively.
Production deployments (the month of March, 30 days):
| ID | Commit | Commit date | Deployment date | Did it cause a failure? |
|---|---|---|---|---|
| d1 | aaa1111 |
2026-03-02 09:00 | 2026-03-05 17:00 | No |
| d2 | bbb2222 |
2026-03-04 14:00 | 2026-03-05 17:00 | No |
| d3 | ccc3333 |
2026-03-06 11:00 | 2026-03-12 17:00 | Yes |
| d4 | ddd4444 |
2026-03-12 18:00 | 2026-03-12 19:30 | No (urgent fix) |
| d5 | eee5555 |
2026-03-10 10:00 | 2026-03-19 17:00 | No |
| d6 | fff6666 |
2026-03-18 16:00 | 2026-03-19 17:00 | No |
| d7 | ggg7777 |
2026-03-20 09:00 | 2026-03-26 17:00 | Yes |
| d8 | hhh8888 |
2026-03-26 18:00 | 2026-03-26 20:00 | No (urgent fix) |
Incidents:
| ID | Start (began to affect users) | Detected | Restored |
|---|---|---|---|
| i1 | 2026-03-12 17:20 | 2026-03-12 18:05 | 2026-03-12 19:35 |
| i2 | 2026-03-26 17:10 | 2026-03-26 17:25 | 2026-03-26 20:05 |
Calculate: deployment frequency (per week), median lead time (in hours), change failure rate and median time to restore. Then answer: what pattern do these figures reveal about how Citalia works?
Exercise 2: spot metric manipulation
Citalia's new CTO has established that each developer's quarterly bonus will depend on "their" individual lead time and "their" deployment frequency. Three months later, the dashboard shows:
- Deployment frequency: from 1.9 to 11.3 per week. 🎉
- Median lead time: from 4.1 days to 3.2 hours. 🎉
- Change failure rate: from 25% to 31%. 😐
- Median time to restore: from 145 min to 210 min. 😟
- Developers mention that they now do most of the work locally and only commit at the end.
- The average number of commits per deployment has dropped from 4.2 to 1.1, but the number of lines changed per deployment has not dropped.
Answer:
- Have they genuinely improved? Justify it with the data.
- What specific behaviour explains each figure?
- Which figure gives the manipulation away most clearly?
- What would you have done instead of tying the bonus to the metrics?
Exercise 3: instrument Reservalia
Design the minimal instrumentation for Reservalia to start measuring today, before having any pipeline.
- Which events must be recorded as a minimum, and at exactly which moment of each process?
- Write the SQL query that gives the median lead time in hours over the last 8 weeks for the
apiservice in production. - Reservalia deploys
apiandwebtogether in the same act. Should that be recorded as one deployment or two? Argue both positions and decide. - In the first month there will only be around 5 deployments. What precaution must you take when interpreting the metrics with so little data?
Solutions
Solution to Exercise 1
Deployment frequency
→ Medium level (between once a month and once a week, close to high).
Lead time per deployment
| ID | Commit | Deployment | Lead time |
|---|---|---|---|
| d1 | 03-02 09:00 | 03-05 17:00 | 80 h |
| d2 | 03-04 14:00 | 03-05 17:00 | 27 h |
| d3 | 03-06 11:00 | 03-12 17:00 | 150 h |
| d4 | 03-12 18:00 | 03-12 19:30 | 1.5 h |
| d5 | 03-10 10:00 | 03-19 17:00 | 223 h |
| d6 | 03-18 16:00 | 03-19 17:00 | 25 h |
| d7 | 03-20 09:00 | 03-26 17:00 | 152 h |
| d8 | 03-26 18:00 | 03-26 20:00 | 2 h |
Sorted: 1.5 · 2 · 25 · 27 · 80 · 150 · 152 · 223
With 8 values, the median is the mean of the two middle ones (4th and 5th):
→ High level (between a day and a week).
Compare with the mean: (1.5+2+25+27+80+150+152+223) / 8 = 82.6 h. That is 55% higher, dragged up by the two extreme values. This illustrates why the median is used.
Change failure rate
→ Medium level.
An important note: d4 and d8 are the urgent fixes, not the failures. They do not count as failed — they fixed the problem — but they do count in the denominator as deployments carried out. Counting them as failures too would double the problem in the metric.
Time to restore service
i1: 03-12 19:35 − 03-12 17:20 = 135 minutes
i2: 03-26 20:05 − 03-26 17:10 = 175 minutes
median = (135 + 175) / 2 = 155 minutes ≈ 2.6 hours→ High level (less than a day).
A detail you had to notice: the "Detected" column is not used. Time counts from when the incident begins to affect users. If it were measured from detection, i1 would give 90 minutes instead of 135 — and Citalia would be rewarding itself for having poor monitoring.
What pattern do the figures reveal?
A very clear and very recognisable pattern:
- They deploy on Thursdays at 17:00. d1, d2, d3, d5, d6 and d7 are all Thursdays at 17:00. They have a weekly deployment window, exactly the same anti-pattern as Reservalia with its Fridays.
- 25% of their deployments are urgent fixes (d4 and d8), and both happen the same day as a failed deployment, a few hours later. In other words: the weekly window generates large batches, the large batches break production, and they have to ship urgently outside the window.
- The window is the cause of the high lead time. Look at the contrast: deployments inside the window have lead times of 25 to 223 hours; the urgent fixes, 1.5 and 2 hours. Technically they can deploy in 90 minutes. The rest of the time is pure waiting, self-imposed by the process.
- Both incidents are a consequence of deployments. Their problem is not the infrastructure: it is how they deliver.
Diagnostic conclusion: Citalia's number one improvement lever is not buying tools or writing more tests. It is removing the weekly Thursday window. That single change would attack lead time, batch size and change failure rate all at once.
Solution to Exercise 2
1. Have they genuinely improved?
No. They have improved the two speed metrics (the ones affecting the bonus) and worsened the two stability ones (the ones that do not). That exact pattern — speed up, stability down — is the classic signature of manipulation, and it is precisely what the set of four metrics is designed to reveal.
Remember the central finding from section 1: in a real improvement, speed and stability improve together. When they diverge in opposite directions, you are not looking at an improvement: you are looking at the indicator being optimised at the expense of what the indicator was supposed to represent.
2. What behaviour explains each figure
| Figure | The behaviour producing it |
|---|---|
| Frequency ×6 | Changes get chopped into artificial deployments so that each developer racks up deployments of their own |
| Lead time from 4.1 days to 3.2 h | Work sits locally for days and is committed just before merging: the clock starts late. Real delivery time has not dropped; only the measured part has |
| Change failure rate 25% → 31% | More deployments with no more verification; and, above all, code that has spent days unintegrated (the integration hell of 01-01) arrives all at once |
| Time to restore 145 → 210 min | With more fragmented deployments and less integration, diagnosing what broke what is harder. On top of that, nobody has an incentive to restore quickly: it does not score |
3. The figure that gives the manipulation away
The number of commits per deployment drops from 4.2 to 1.1 while the lines changed per deployment stay the same.
This figure is devastating and worth pausing on. If the batches were genuinely smaller, both numbers would have dropped: fewer commits and fewer lines per deployment. The lines not dropping means that the same amount of change is being delivered, packaged into fewer commits. In other words: people accumulate work locally and dump it in a single giant commit just before merging.
That is exactly the opposite of continuous integration, and it coherently explains all four figures at once: longer-lived branches → later integration → more conflicts and less verification → more failures → slower diagnosis.
4. What should have been done
- Never tie the metrics to pay or to individual performance reviews. That is the rule that was broken and from which everything else follows.
- Measure at team or service level, never at person level. Lead time depends on how fast reviews happen, on approval processes and on the deployment window: almost none of that is controlled by an individual.
- Use the metrics to diagnose, not to judge. The analysis in exercise 1 showed that Citalia's problem was the weekly Thursday window. The right action was to remove it, not to hand out bonuses.
- Always present all four together. If the CTO's dashboard had shown all four from the start, the divergence would have been obvious at the first monthly review.
- Set system targets, not personal ones. "Reduce the team's lead time below 8 hours by removing the deployment window" is an actionable, shared objective. "Every developer should lower their lead time" is not.
Solution to Exercise 3
1. Minimal events and exact timing
| Event | Exactly when it is recorded | Essential fields |
|---|---|---|
| Production deployment | As the last step of the deployment process, only if it finished successfully | id, service, environment, commit_sha, commit_date, deployed_at, commits_included |
| Failed deployment flag | During the incident, as soon as the guilty deployment is identified | deployment_id, failed = true, reason |
| Incident opened | When the time it began to affect users is determined (not the detection time) | id, service, started_at, severity, deployment_id if applicable |
| Incident closed | When the service works again for users (not when the cause is understood) | restored_at |
A critical detail: commit_date must be captured at deployment time with git show -s --format=%cI, not looked up afterwards. Branches get deleted and histories get rewritten.
Until a pipeline exists, these events are recorded by hand with a two-line script run by whoever deploys. It is not elegant, but it produces real data from today, which is what matters.
2. SQL query
SELECT
ROUND(
PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (deployed_at - commit_date))
) / 3600.0, 1
) AS lead_time_median_hours,
COUNT(*) AS n_deployments -- essential for knowing whether the figure is worth anything
FROM deployments
WHERE service = 'api'
AND environment = 'prod'
AND result = 'success'
AND deployed_at >= now() - interval '8 weeks';The COUNT(*) is not optional: a median calculated over 5 deployments must not be presented the same way as one calculated over 200.
3. One deployment or two?
In favour of recording it as two (one per service):
- It is what the DORA framework recommends: the unit of measurement is the deployable service.
- It lets you see whether one service evolves differently from the other (for example, whether the web app gets deployed more often once they become independent).
- If they are separated in the future — which is very likely — the historical series remains comparable.
In favour of recording it as one:
- It reflects the current reality: today they cannot be deployed separately, so two rows suggest an independence that does not exist.
- It would artificially double the deployment frequency, making Reservalia look twice as fast as it is.
Decision: record two rows, one per service, but with a shared deployment_group field linking them. That preserves per-service granularity for the future while allowing frequency to be calculated by counting distinct groups rather than rows, without inflating the number. It is the option that destroys no information and deceives nobody.
-- REAL deployment frequency (deployment acts, not rows)
SELECT COUNT(DISTINCT deployment_group) FROM deployments
WHERE environment = 'prod' AND result = 'success';4. Precautions with little data
- With 5 deployments, the median is very unstable. A single extreme value shifts it completely. Do not make decisions based on the variation from one month to the next.
- The change failure rate is especially misleading. With 5 deployments, each failure is worth 20%: going from 0% to 20% sounds like a catastrophe and it is a single incident. At these volumes, it is more informative to report the absolute number ("1 of 5") than the percentage.
- Use longer windows. With low volume, aggregate by quarter instead of by week, or use 90-day moving averages.
- Always show the sample size next to each metric. A dashboard saying "change failure rate: 20%" without saying "(1 of 5)" invites wrong conclusions.
- Look at the trend, not the individual value. With little data, the direction of movement over several months is far more reliable than any specific figure.
- And something reassuring: as Reservalia progresses through the course, deployment frequency will rise, and with it the volume of data. The metrics become more reliable precisely as the process improves.
Conclusion
With this lesson we close the introductory module, and we do so with what is probably the most practical tool in the whole module:
- The four DORA metrics — deployment frequency, lead time for changes, change failure rate and time to restore service — summarise delivery performance in two pairs that balance each other: speed and stability. Their great virtue is that they cannot all be gamed at once.
- The finding that changes the way you think: speed and stability go together. Deploying more often forces small batches, and small batches fail less and are diagnosed faster.
- The fifth metric, reliability, measures the operational outcome through service level objectives each team defines for itself, and avoids the illusion of having all four in the green with a poor service.
- Every data point comes from a specific event: frequency and lead time from the deployment job and the commit date; the change failure rate from a human flag during the incident; time to restore from incident management. It all fits in two tables and a handful of SQL queries: you do not need to buy anything.
- The performance ranges are references that move every year and depend on context. The only comparison that matters is with yourself three months ago.
- Reservalia now has its baseline measured: 1.1 deployments a week, 6.2 days of lead time, 14% failed changes and 68 minutes to restore. And its targets for the end of the course: 5 deployments a week, under 4 hours, under 5% and under 10 minutes.
- And the warning that underpins everything else: Goodhart's law. As soon as a metric becomes an individual target — tied to reviews or bonuses — it stops describing reality. They are system metrics, they are for diagnosis, and you look at all four together.
That is the end of module 1. You now know what continuous integration, continuous delivery and continuous deployment are and how they differ; you know the real benefits and the costs nobody mentions; you have the map of the tool ecosystem and you know why we will use GitHub Actions; you know Reservalia in depth, its team, its repository and its target infrastructure; and you have the dashboard that will tell us, seven modules from now, whether all this has been any use.
Theory is over. In module 2, Continuous Integration (CI), we start building: the first lesson, Introduction to Continuous Integration, lays down the team's working rules — what the main branch is, how long a branch may live, what happens when the build turns red — and in lesson 02-02 we will finally write Reservalia's first real workflow. Diego will stop building on his laptop: from then on, every change verifies itself, on a clean machine, before anybody merges it.
CI/CD Course: Continuous Integration and Deployment
Module 1: Introduction to CI/CD
- Basic CI/CD Concepts
- Benefits of CI/CD
- Popular CI/CD Tools
- The Course Project: the Application We Are Going to Automate
- DORA Metrics: How Software Delivery Is Measured
Module 2: Continuous Integration (CI)
- Introduction to Continuous Integration
- Setting Up a CI Environment
- Build Automation
- Automated Testing
- Code Quality and Static Analysis
- Artifacts, Versioning and Promotion
- Integration with Version Control
Module 3: Continuous Deployment (CD)
- Introduction to Continuous Deployment
- Deployment Automation
- Infrastructure as Code and Reproducible Environments
- Deployment Strategies
- Feature Flags, Rollback and Failure Recovery
- Monitoring and Feedback
Module 4: Advanced CI/CD Practices
- CI/CD Pipelines
- Dependency Management
- Security in CI/CD
- Scalability and Performance
- Pipeline as Code: Templates, Reuse and Testing the Pipeline
- Databases in the Pipeline: Safe Migrations
Module 5: Implementing CI/CD in Real Projects
- Case Study: Web Project
- Case Study: Mobile Application
- Case Study: Microservices
- Case Study: Modernising a Legacy Project
Module 6: Tools and Technologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker and Kubernetes
- GitHub Actions in Depth
- Comparison and Criteria for Choosing a Tool
Module 7: Practical Exercises
- Exercise 1: Setting Up a Basic Pipeline
- Exercise 2: Integrating Automated Tests
- Exercise 3: Deploying to a Production Environment
- Exercise 4: Monitoring and Feedback
- Exercise 5: Hardening the Pipeline with Security and Secrets
- Final Project: A Complete End-to-End Pipeline
