DORA-metrics voor Rails-teams: Deployment Frequency, Lead Time en Change Failure Rate meten
DORA-metrics voor Rails-teams: meet deployment frequency, lead time en change failure rate met GitHub Actions en ActiveRecord. Met werkende code.
Een paar maanden geleden werd ik gevraagd om een team van veertig mensen te beoordelen dat trots was op zijn proces. Twee Jira-borden vol met groene vinkjes. Afgesloten sprints overal. “We shippen elke twee weken,” zei de CTO, met de overtuiging van iemand die net een retro had gehad die goed liep.
Daarna trok ik de data.
Gemiddelde tijd van eerste commit op een branch tot die commit in productie: drieëntwintig dagen. Change failure rate over het afgelopen kwartaal: 41 procent. Mean time to restore na een incident: vier uur en twaalf minuten. Ze dachten dat ze goed presteerden. Gemeten met DORA-metrics zaten ze stevig in de Low-band, op z’n best aan de grens van Medium. De sprints waren groen omdat engineers Jira-tickets sloten, niet omdat software werd uitgeleverd. Dat zijn twee verschillende dingen.
De DORA-metrics — Deployment Frequency, Lead Time for Changes, Change Failure Rate en Time to Restore Service — komen uit het DORA-onderzoeksprogramma en werden gepubliceerd door Nicole Forsgren, Jez Humble en Gene Kim in “Accelerate” (2018). Het zijn de meest rigoureus gevalideerde softwareleveringsmetingen die we hebben. Het onderzoek toont aan dat ze niet alleen correleren met leveringsprestaties, maar ook met organisatorische uitkomsten: omzetgroei, marktaandeel en of engineers bij het bedrijf blijven of vertrekken naar een plek die wél levert. Voor elke startup waarvoor ik als fractional CTO werk, is het instrumenteren van deze vier cijfers een van de eerste dingen die ik doe. Niet omdat ik dashboards wil bijhouden, maar omdat je geen eerlijk gesprek kunt voeren over de gezondheid van een engineeringteam totdat je weet waar het werkelijk staat.
Dit artikel beschrijft de Rails-implementatie: het datamodel, de GitHub Actions-integratie, de ActiveRecord-queries voor elke metric en het minimale admin-dashboard dat je in zestig seconden vertelt of je team vooruitgaat of achteruitgaat.
Wat DORA-metrics zijn (en wat ze niet zijn)
DORA-metrics zijn uitkomsten, geen activiteiten. Ze tellen geen commits, story points, regels code of testdekking in procenten. Ze meten wat de shipmachine produceert: hoe vaak het levert, hoe snel, hoe betrouwbaar en hoe snel het herstelt. Dat onderscheid is belangrijk, want activiteitsmetrics zijn eenvoudig te manipuleren. Een team kan duizend commits per maand produceren en nooit iets uitleveren dat een gebruiker ziet. Uitkomstmetrics beschrijven wat er werkelijk in productie is gebeurd.
De vier metrics en hun prestatiebanden, uit het State of DevOps-rapport van 2024:
| Metric | Elite | High | Medium | Low |
|---|---|---|---|---|
| Deployment Frequency | Meerdere keren per dag | Eén keer per dag tot één keer per week | Één keer per week tot één keer per maand | Minder dan één keer per maand |
| Lead Time for Changes | Minder dan een uur | Tussen één dag en één week | Tussen één week en één maand | Tussen één maand en zes maanden |
| Change Failure Rate | Minder dan 5% | 5–10% | 11–15% | Meer dan 15% |
| Time to Restore Service | Minder dan een uur | Minder dan een dag | Minder dan een dag | Tussen een dag en een week |
De meeste vroege Rails-teams scoren Medium op deployment frequency en Low op lead time. Change failure rate is de metric waar niemand naar wil kijken, omdat die vereist dat je toegeeft dat je incidenten hebt. Begin daar toch mee.
Deployment Frequency: Elke Productie-deploy Bijhouden
Deployment frequency is niet “hoe vaak we pull requests mergen.” Het is hoe vaak code de omgeving bereikt waar echte gebruikers op zitten. Op een Rails-team dat GitHub Actions voor CI/CD gebruikt, werkt een webhook-stap aan het einde van je deploy-workflow het beste: die stuurt een melding naar je eigen Rails-applicatie.
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... jouw build-, test- en deploystappen ...
- name: Record deployment
if: success()
run: |
curl -s -X POST "$/webhooks/deployments" \
-H "Authorization: Bearer $" \
-H "Content-Type: application/json" \
-d '{
"sha": "$",
"ref": "$",
"actor": "$",
"run_id": "$"
}'
- name: Record rollback
if: failure()
run: |
curl -s -X POST "$/webhooks/deployments" \
-H "Authorization: Bearer $" \
-H "Content-Type: application/json" \
-d '{
"sha": "$",
"ref": "$",
"actor": "$",
"run_id": "$",
"status": "failed"
}'
Aan de Rails-kant slaat een Deployment-model elke gebeurtenis op:
# db/migrate/20260630000001_create_deployments.rb
class CreateDeployments < ActiveRecord::Migration[8.0]
def change
create_table :deployments do |t|
t.string :sha, null: false
t.string :ref, null: false, default: "main"
t.string :actor
t.string :github_run_id
t.string :status, null: false, default: "success" # success, failed, rollback
t.integer :lead_time_seconds
t.integer :commit_count
t.jsonb :metadata, null: false, default: {}
t.datetime :deployed_at, null: false
t.timestamps
end
add_index :deployments, :deployed_at
add_index :deployments, :sha, unique: true
add_index :deployments, [:status, :deployed_at]
end
end
De webhook-controller authenticeert met een bearer token uit credentials en maakt het record aan:
# app/controllers/webhooks/deployments_controller.rb
class Webhooks::DeploymentsController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_deploy_token!
def create
deployment = Deployment.create!(
sha: params[:sha],
ref: params[:ref],
actor: params[:actor],
github_run_id: params[:run_id],
status: params[:status] || "success",
deployed_at: Time.current
)
DeploymentEnricherJob.perform_later(deployment.id)
render json: { id: deployment.id }, status: :created
rescue ActiveRecord::RecordNotUnique
head :ok # idempotent: zelfde SHA al geregistreerd
end
private
def authenticate_deploy_token!
token = request.headers["Authorization"].to_s.delete_prefix("Bearer ")
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(
token, Rails.application.credentials.deploy_webhook_token!
)
end
end
Deployment frequency is daarna één query:
# app/models/concerns/dora/deployment_frequency.rb
module Dora
module DeploymentFrequency
def self.deploys_per_day(period: 30.days.ago..Time.current)
count = Deployment.where(deployed_at: period, status: "success").count
days = (period.end - period.begin) / 1.day
count.to_f / days.to_f
end
def self.band(deploys_per_day)
case deploys_per_day
when (1.0..) then "Elite"
when (1.0 / 7..1.0) then "High"
when (1.0 / 30..1.0 / 7) then "Medium"
else "Low"
end
end
end
end
Lead Time for Changes: Waar de Echte Knelpunten Zitten
Lead Time for Changes meet de tijd van de eerste commit van een wijziging tot die commit in productie staat. Niet “hoe lang duurt een sprint.” De wandkloktijd van “een engineer schreef een regel code” tot “een gebruiker kan het zien.” Lange lead times betekenen bijna altijd batch-releases, langlevende branches of een reviewproces dat niemand meer serieus neemt.
Om dit te meten heb je informatie nodig over welke commits er in elke deploy zaten. Een achtergrondtaak haalt dit op via de GitHub API nadat elke deploy is geregistreerd:
# app/jobs/deployment_enricher_job.rb
class DeploymentEnricherJob < ApplicationJob
queue_as :default
def perform(deployment_id)
deployment = Deployment.find(deployment_id)
previous_sha = Deployment
.where(deployed_at: ...deployment.deployed_at, status: "success")
.order(deployed_at: :desc)
.pick(:sha)
return unless previous_sha
commits = GithubClient.commits_between(
base: previous_sha,
head: deployment.sha
)
return if commits.empty?
first_commit_at = commits.map { |c| c[:authored_at] }.min
return unless first_commit_at
deployment.update!(
lead_time_seconds: (deployment.deployed_at - first_commit_at).to_i,
commit_count: commits.size,
metadata: deployment.metadata.merge(
"first_commit_sha" => commits.last[:sha],
"first_commit_at" => first_commit_at.iso8601
)
)
end
end
De GitHub compare-API doet het zware werk:
# app/services/github_client.rb
class GithubClient
REPO = Rails.application.credentials.dig(:github, :repo)
TOKEN = Rails.application.credentials.dig(:github, :token)
def self.commits_between(base:, head:)
conn = Faraday.new("https://api.github.com") do |f|
f.response :raise_error
f.response :json
end
response = conn.get(
"/repos/#{REPO}/compare/#{base}...#{head}",
{},
{
"Authorization" => "Bearer #{TOKEN}",
"Accept" => "application/vnd.github+json",
"X-GitHub-Api-Version" => "2022-11-28"
}
)
response.body.fetch("commits", []).map do |c|
{
sha: c["sha"],
authored_at: Time.zone.parse(c.dig("commit", "author", "date"))
}
end
rescue Faraday::Error => e
Rails.logger.error("GithubClient#commits_between mislukt: #{e.message}")
[]
end
end
Lead time-queries:
module Dora
module LeadTime
def self.median_seconds(period: 30.days.ago..Time.current)
times = Deployment
.where(deployed_at: period, status: "success")
.where.not(lead_time_seconds: nil)
.order(:lead_time_seconds)
.pluck(:lead_time_seconds)
return nil if times.empty?
times[times.size / 2]
end
def self.band(seconds)
return "Onbekend" if seconds.nil?
case seconds
when (0...3_600) then "Elite" # minder dan een uur
when (3_600...86_400) then "High" # minder dan een dag
when (86_400...604_800) then "Medium" # minder dan een week
else "Low"
end
end
end
end
Gebruik de mediaan, niet het gemiddelde. Één deploy die drie weken commits heeft gebundeld blaast je gemiddelde op en verbergt hoe een normale deploy er eigenlijk uitziet.
De uitsplitsing die ik als eerste bekijk is niet de totale lead time, maar waar de tijd naartoe gaat: codetijd (eerste commit tot PR geopend), reviewtijd (PR geopend tot gemerged) en wachttijd (merge tot productie). De meeste Rails-teams hebben normale code- en wachttijden en een enorme reviewtijd — pull requests die drie dagen open staan omdat niemand context heeft, iedereen het druk heeft en er geen cultuur is van code review als urgente taak. Dat aanpakken kost niets en halveert je lead time gewoonlijk binnen een maand.
Change Failure Rate: De Metric Waar Niemand Naar Wil Kijken
Change Failure Rate is het percentage deploys dat een incident veroorzaakt, een hotfix vereist of teruggedraaid moet worden. Teams tellen dit systematisch te laag in, omdat ze alleen de grote incidenten loggen — de incidenten die iemand om 3 uur ‘s nachts wakker maken. Maar de terugdraaiactie om 3 uur is het gevolg van de change failure; de upstream-gebeurtenis is een deploy waarbij iets stilletjes kapot ging en een klant het de volgende ochtend meldde. Als dat een hotfix-PR genereerde, telt het mee.
Voeg een manier toe om een deploy achteraf als mislukt te markeren. Ik doe dit via een aparte webhook-aanroep vanuit een rollback-actie of een Slack-incidentcommando:
# app/controllers/webhooks/deployment_failures_controller.rb
class Webhooks::DeploymentFailuresController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_deploy_token!
def create
deployment = Deployment.find_by!(sha: params[:sha])
deployment.update!(
status: "rollback",
metadata: deployment.metadata.merge("failure_reason" => params[:reason])
)
Incident.create!(
deployment: deployment,
severity: params[:severity] || "p2",
source: "deploy",
detected_at: deployment.deployed_at
)
head :ok
end
private
def authenticate_deploy_token!
token = request.headers["Authorization"].to_s.delete_prefix("Bearer ")
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(
token, Rails.application.credentials.deploy_webhook_token!
)
end
end
Change failure rate-query:
module Dora
module ChangeFailureRate
def self.call(period: 30.days.ago..Time.current)
scope = Deployment.where(deployed_at: period)
total = scope.count
failures = scope.where(status: "rollback").count
return 0.0 if total.zero?
(failures.to_f / total * 100).round(1)
end
def self.band(rate)
case rate
when (0...5) then "Elite"
when (5...10) then "High"
when (10..15) then "Medium"
else "Low"
end
end
end
end
Een change failure rate van 10% klinkt beheersbaar totdat je de wiskunde doet. Bij wekelijkse deploys zijn dat vijf of zes incidenten per kwartaal, elk met een hotfix-cyclus, postmortem en klantcommunicatie. Bij dagelijkse deploys is dat je fulltime baan. Het contra-intuïtieve resultaat uit het Accelerate-onderzoek — en ik heb dit herhaaldelijk bij klantteams zien spelen — is dat teams die vaker deployen een lagere change failure rate hebben, niet hoger. Kleinere batches betekenen een kleiner risicogebied. Een wijziging van twee bestanden is gemakkelijker te reviewen, gemakkelijker te testen en sneller terug te draaien dan een feature van 47 bestanden die drie weken is opgebouwd.
Time to Restore Service: Veerkracht Meten
Time to Restore Service (TTRS) is hoe lang het duurt om het systeem terug te brengen naar normaal na een storing. Voor een Rails-team betekent dit gewoonlijk het interval van “eerste alert afgaat of eerste klant meldt een probleem” tot “de fix of rollback is uitgeleverd en de monitors staan op groen.” Sla dit op in een aparte incidents-tabel, want niet elk incident wordt veroorzaakt door een deploy en je wilt de flexibiliteit om beide typen te registreren:
# db/migrate/20260630000002_create_incidents.rb
class CreateIncidents < ActiveRecord::Migration[8.0]
def change
create_table :incidents do |t|
t.references :deployment, foreign_key: true
t.string :severity, null: false, default: "p2" # p1, p2, p3
t.string :source, null: false, default: "manual" # deploy, alert, customer
t.datetime :detected_at, null: false
t.datetime :resolved_at
t.integer :ttrs_seconds
t.jsonb :metadata, null: false, default: {}
t.timestamps
end
add_index :incidents, :detected_at
add_index :incidents, :resolved_at
end
end
Een before_save callback berekent TTRS wanneer het incident wordt gesloten:
# app/models/incident.rb
class Incident < ApplicationRecord
belongs_to :deployment, optional: true
before_save :calculate_ttrs, if: -> { resolved_at_changed? && resolved_at.present? }
scope :p1, -> { where(severity: "p1") }
scope :resolved, -> { where.not(resolved_at: nil) }
private
def calculate_ttrs
self.ttrs_seconds = (resolved_at - detected_at).to_i
end
end
TTRS-query:
module Dora
module TimeToRestore
def self.median_seconds(period: 30.days.ago..Time.current, severity: nil)
scope = Incident.where(detected_at: period).resolved
scope = scope.where(severity: severity) if severity
times = scope.order(:ttrs_seconds).pluck(:ttrs_seconds).compact
return nil if times.empty?
times[times.size / 2]
end
def self.band(seconds)
return "Onbekend" if seconds.nil?
case seconds
when (0...3_600) then "Elite" # minder dan een uur
when (0...86_400) then "High" # minder dan een dag
else "Low"
end
end
end
end
Volg P1-incidenten apart van P2 en P3. Een TTRS van vier uur voor een P3 (klein visueel probleem) en een TTRS van vier uur voor een P1 (betalingen liggen stil) zijn heel verschillende verhalen. De DORA-band geldt voor incidenten die de dienst voor gebruikers verstoren; kleine problemen die worden verholpen in de volgende normale deploy tellen niet mee.
Een Minimaal DORA-dashboard in Rails
Een admin-controller die alle vier de metrics tegelijk ophaalt — vier queries, allemaal gedekt door de indexes, allemaal onder de 100 ms op een tabel met miljoenen rijen:
# app/controllers/admin/dora_controller.rb
class Admin::DoraController < Admin::BaseController
PERIOD = 30.days.ago..Time.current
def show
df = Dora::DeploymentFrequency.deploys_per_day(period: PERIOD)
lt = Dora::LeadTime.median_seconds(period: PERIOD)
cfr = Dora::ChangeFailureRate.call(period: PERIOD)
ttr = Dora::TimeToRestore.median_seconds(period: PERIOD)
@metrics = [
{
name: "Deployment Frequency",
value: "#{df.round(2)} / dag",
band: Dora::DeploymentFrequency.band(df)
},
{
name: "Lead Time for Changes",
value: lt ? ActiveSupport::Duration.build(lt).inspect : "n.v.t.",
band: Dora::LeadTime.band(lt)
},
{
name: "Change Failure Rate",
value: "#{cfr}%",
band: Dora::ChangeFailureRate.band(cfr)
},
{
name: "Time to Restore",
value: ttr ? ActiveSupport::Duration.build(ttr).inspect : "n.v.t.",
band: Dora::TimeToRestore.band(ttr)
}
]
end
end
In de view kleur je de bandkolom: groen voor Elite, turkoois voor High, amber voor Medium, rood voor Low. Een tabel die je in één oogopslag kunt lezen tijdens de wekelijkse engineeringmeeting is beter dan een custom analytics-dashboard dat je eeuwig onderhoudt. Ik koppelde dit onlangs aan OpenTelemetry-tracing voor een klant — OpenTelemetry beantwoordt vragen over je draaiende systeem (latency, foutpercentage, doorvoer), DORA beantwoordt vragen over je leveringsproces. Samen dekken ze de volledige cyclus.
Benchmarks en Wat Je Met de Cijfers Doet
De vier banden zijn een startpunt, geen eindpunt. Teams belanden in Low om verschillende redenen en de oplossing is elke keer anders.
Een team met lage deployment frequency en lage change failure rate heeft meestal zijn goedkeuringsprocessen overdreven. Ze zijn zorgvuldig maar traag, en “zorgvuldig” doet minder veiligheidswerk dan ze denken, omdat infrequente deploys risico’s concentreren. De eerste tien commits van een sprint zijn misschien prima; de tweeënveertigste commit — die twee weken later tegelijk met alles andere wordt uitgeleverd — is de commit die iets kapot maakt dat niemand heeft getest.
Een team met hoge deployment frequency en een change failure rate van 25% heeft het omgekeerde probleem. Ze shippen snel, maken regelmatig iets stuk en hun vertrouwen is misplaatst. De oplossing hier is bijna altijd testdekking voor de specifieke paden die steeds falen, niet het vertragen van de deploys.
De lead time-uitsplitsing is het eerste dat ik bekijk na het ophalen van de basismetingen. Splits je lead time op in drie onderdelen: tijd van eerste commit tot PR geopend, tijd van PR geopend tot gemerged en tijd van merge tot productie. De meeste Rails-teams hebben normale codetijd, een CI-pipeline van 10–20 minuten en een reviewvenster van 48–72 uur omdat iedereen het druk heeft en niemand code review als urgent behandelt. Dat laatste getal is het gemakkelijkst aan te pakken — een teamafspraak dat PR’s van onder de 200 regels dezelfde dag worden gereviewd, halveert de totale lead time gewoonlijk zonder dat er verder iets verandert.
Analyse van de change failure rate moet verder gaan dan “we hadden X rollbacks.” Welke deploys mislukken? Als 80% van je mislukkingen één specifiek domeinmodel of één legacy-service raken, is dat een ander probleem dan mislukkingen die gelijkmatig verdeeld zijn over de codebase. Het technical due diligence-framework dat ik bij klanten gebruik, omvat het ophalen van deze correlatie uit deploy- en incidentdata als onderdeel van de eerste sessie.
Eén ding dat het Accelerate-onderzoek aantoonde — en dat teams nog steeds verrast als ik het laat zien — is dat vaker deployen de change failure rate verbetert. De causaliteit loopt beide kanten op. Teams die gedwongen worden kleinere stukken te shippen door een cultuur van dagelijkse deploys, produceren wijzigingen die gemakkelijker te reviewen, gemakkelijker te testen, gemakkelijker terug te draaien en dus minder waarschijnlijk te falen zijn. De shipmentdiscipline is het veiligheidsmechanisme.
Veelgestelde Vragen Over DORA-metrics voor Rails-teams
Hoe lang duurt het om zinvolle DORA-data te verzamelen?
Dertig dagen geeft je een basislijn, negentig dagen geeft je een trend. De eerste twee weken zijn rommelig — vakantieperiodes, een grote geplande migratie, een week waarbij het team volledig op een lancering was gericht. Commit je aan meten gedurende een volledig kwartaal voordat je conclusies trekt of doelen stelt.
Moeten we DORA-metrics per service of per team bijhouden?
Beide, maar begin per team. Als je één Rails-monoliet en één cross-functioneel team hebt, is dat je eenheid. Als je meerdere services of subteams hebt, volg ze dan apart en aggregeer niet — een hoge failure rate in de betalingsservice die verdund wordt door een lage failure rate in de contentservice is precies het soort signaal dat je duidelijk wilt zien.
Wat telt als een deployment voor DORA-doeleinden?
Elke wijziging die de omgeving bereikt waar echte gebruikers op zitten. Hotfixes tellen mee. Configuratiewijzigingen die via je normale deploypipeline worden doorgevoerd, tellen mee. Feature flags die nieuwe codepaden inschakelen, tellen mee als de vlagwijziging zelf via een deploy gaat. Zero-downtime database-migraties die samen met applicatiecode worden uitgeleverd, tellen mee. Wat niet meetelt: wijzigingen die alleen naar staging gaan, CMS-contentupdates die je deploypipeline omzeilen, of infrastructuurwijzigingen waarbij je applicatiecode niet betrokken is.
Hoe ga ik om met teams die meerdere keren per dag deployen?
Mooi probleem om te hebben. Bij meerdere deploys per dag wordt lead time de meest informatieve metric, omdat deployment frequency al Elite is en de bottleneck ergens in je workflow vóór de deployknop zit. Splits lead time op per auteur, per PR-grootte en per tijdstip van de dag. De bottleneck zit bijna altijd in reviewlatency of in een CI-stap die niemand heeft geoptimaliseerd sinds die twee jaar geleden werd toegevoegd.
Die CTO die ik aan het begin noemde — 23 dagen lead time, 41% change failure rate — nam de cijfers terug naar zijn team en had de meest productieve engineeringretro die ze ooit hadden gehad. Zes maanden later was de lead time onder de vijf dagen en de change failure rate onder de 10%. Geen Elite. Niet eens High op elke metric. Maar een team dat de waarheid over zichzelf kende en in de goede richting bewoog, wat meer is dan de meeste teams.
Iemand nodig die je DORA-metrics instrumenteert en je eerlijk vertelt wat ze betekenen? TTB Software is gespecialiseerd in engineering-assessments en fractional technisch leiderschap voor Rails-teams bij groeibedrijven. We doen dit al negentien jaar.
Related Articles
Rails Technical Due Diligence: De Fractional CTO-Checklist voor Kopers en Investeerders
Rails technical due diligence checklist van een fractional CTO. Wat te auditen voor je een Rails-app koopt of erin in...
Build vs Buy Software: Een fractional CTO-framework voor engineeringbeslissingen
Build vs buy software-beslissingen kunnen een startup nekken. Een fractional CTO-framework voor vendors, total cost e...
Senior Rails Engineer Interview: De Wervingsrubric van een Fractional CTO voor 2026
Senior Rails Engineer Interview rubric van een fractional CTO met 19 jaar Rails. De vragen, scoring, signalen en red ...