CI pipeline failure analysis
How to analyze CI pipeline failures: classify them, fingerprint recurring errors, measure MTTR, and rank fixes by cost — from pipeline metadata alone, no code access.
CI pipeline failure analysis is the practice of treating build failures as a dataset instead of a stream of interruptions. It has four moving parts: classify every failed run (code, infrastructure, flaky, or cancelled), group recurring errors into fingerprints so the same problem is counted once instead of a hundred times, measure how long each problem stays unresolved, and rank the results by what they cost in compute minutes and engineer time — not by how often they fire.
Everything in that loop can be computed from data your CI provider already exposes: run and job conclusions, timestamps, durations, attempt counts, and the log output of failed jobs. No source code, workflow files, or commit diffs are required. This guide walks through the full method — why ad-hoc triage fails, a four-bucket failure taxonomy, correct failure-rate denominators, error fingerprinting, MTTR, dollar-cost ranking, and a weekly triage workflow — and closes with exactly which questions pipeline metadata can answer on its own.
Why ad-hoc triage fails
On most teams, pipeline failure handling is an accident of who happened to be blocked. A build goes red, the engineer who needs to merge investigates just far enough to get unblocked, presses re-run, and moves on the moment the status turns green. Locally that is rational — their job is to ship a change, not to run a reliability program. Globally it fails, for four structural reasons.
Recency bias sets the priorities. The failure that gets attention is the one that interrupted someone today. Chronic failures get normalized instead of fixed: after the third week, "that suite is just flaky, re-run it" becomes tribal knowledge, and the most expensive problem in the pipeline turns invisible precisely because everyone sees it daily.
Re-running erases the evidence. When a retry turns the run green, the final status reads success and the failure is never counted anywhere. The team pays the retry tax — duplicate compute plus the wait — without ever booking it. A failure mode that clears on the second attempt most of the time can persist for years, because at any given moment fixing it feels less urgent than the work it just interrupted.
Frequency gets mistaken for importance. The failure people complain about most is the one they see most often, and that is usually a fast, cheap job early in the pipeline. A lint step that fails two hundred times a month at 40 seconds per failure generates far more chat noise than an integration suite that fails thirty times at 25 minutes and is usually retried once — yet the suite burns roughly ten times the compute and inflicts much longer waits.
Human memory is the database. Ad-hoc triage runs on what people remember, which in practice rarely reaches back more than a week or two. Failures that recur on longer cycles — a dependency bump every quarter, a runner image update once a month, load-related timeouts around release day — look like one-off events every single time they happen.
The rest of this guide is the antidote: a small amount of structure, applied consistently, to data the pipeline already produces.
A failure taxonomy
Actionable triage starts by sorting failures into buckets that differ in fix and owner. Four buckets are enough for CI:
| Category | Signature in pipeline metadata | Typical fix | Usual owner |
|---|---|---|---|
| Code failure | Deterministic: the same commit fails on every attempt and clears only when a new commit lands | Fix forward or revert | Author of the change |
| Infrastructure failure | Fails across unrelated branches or repositories in the same time window; log tails show network timeouts, registry errors, or runner provisioning problems; clears without any code change | Fix the platform, not the code | Platform or DevOps |
| Flaky failure | The same commit both passes and fails; fail-then-pass alternation on one branch; retries usually clear it | Quarantine, then de-flake | Test or component owner |
| Cancelled run | Stopped before producing a verdict: superseded push, concurrency group, manual stop | Usually nothing — keep it countable on its own and state how your tool treats it | Nobody, by design |
The buckets matter because each routes differently. A code failure belongs to the author and resolves through the normal review loop. An infrastructure failure filed against the test author bounces around until someone notices the same error hitting five unrelated repositories at once. A flaky failure needs a containment decision — quarantine or fix — rather than a revert. And a cancelled run needs nothing at all: cancellations are mostly a sign that concurrency settings are working, and silently folding them into a failure rate without saying so distorts every downstream metric — the choice itself is defensible either way, but it has to be stated (TrimCI counts them on the unhealthy side). The distinction is worth being precise about — see cancelled vs. failed runs.
One provider asymmetry to know: GitHub Actions reports timeouts as a distinct timed_out
conclusion (REST API reference), while
GitLab CI has no equivalent status — a timed-out job simply ends as failed. GitLab exposes the
distinction one level down instead, in the job object's failure_reason field
(stuck_or_timeout_failure, job_execution_timeout — Jobs API,
checked 2026-07-28). Aggregate across both providers on run conclusions alone and a "timeout"
category silently undercounts on the GitLab side; read failure_reason per job, falling back to
log-tail classification where it is absent, to close the gap.
Bucketing does not require reading anyone's code. Determinism is visible from repeated attempts on the same commit, cross-repo correlation is visible from timestamps, and the error class is visible in the failed job's log tail.
Get the denominator right
A failure rate is only as trustworthy as its denominator, and the naive version — failures divided by all runs — is distorted by runs that never produced a verdict.
Take an illustrative month: 1,000 runs, of which 280 were cancelled by concurrency groups when a newer push superseded them, 20 were skipped, 80 failed, and 620 succeeded. Failures over all runs gives 8%. But the question you actually care about — when the pipeline ran to completion, how often did it fail? — is 80 out of 700, or 11.4%. The right unit is the decisive run: a run that ended in success or failure. Skipped and neutral runs carry no information about whether the code was good, so they belong in neither the numerator nor the denominator. Cancellations are the one judgement call, and tools split on it: dropping them entirely — the arithmetic above — keeps the rate a pure statement about code, while counting them on the unhealthy side (TrimCI's choice) keeps a pipeline that never delivered an answer visible. Pick one, apply it everywhere, and say which one you picked; a repository that auto-cancels superseded pushes reads very differently under the two.
Two rules keep the number honest:
- One denominator everywhere. The headline failure rate, the per-repository breakdown, the per-provider comparison, and the week-over-week trend must all use decisive runs. Mixing definitions produces artifacts — a repository can look like it improved simply because a concurrency setting started cancelling more superseded runs. The same discipline applies to the complementary pipeline success rate.
- Track cancellation volume separately. A high cancellation share is not a reliability problem, but it is a signal — of push churn, of aggressive auto-cancellation, sometimes of developers manually killing slow pipelines. Watch it as its own metric instead of letting it contaminate the failure rate.
Fingerprint recurring errors
One hundred thirty-seven failed runs in a month is not 137 problems. It is usually a handful of problems recurring at different rates, and the way to see that is a failure fingerprint: a normalized signature extracted from the failed job's error output. Strip the volatile parts — timestamps, commit identifiers, temporary paths, host names, run numbers — and keep the stable skeleton: the error class, the failing step, the shape of the message. Two failures with the same skeleton are the same underlying problem.
Fingerprinting buys three things:
- Deduplication. The raw failure list collapses into a short list of distinct problems, and in practice a small minority of fingerprints accounts for the large majority of failures. That short list is the actual triage queue.
- Automatic bucketing evidence. A fingerprint that appears across many repositories in the same hour is almost certainly infrastructure. A fingerprint pinned to one branch that alternates with passes is flaky. A fingerprint that appears with one commit and dies with the next was a code failure. The grouping does the taxonomy work for you.
- Trend per problem. With failures grouped, you can ask whether a specific problem is new, worsening, or actually fixed — instead of staring at an aggregate failure rate that moves for ten reasons at once.
Be honest about the granularity. Fingerprinting run and job metadata tells you which error keeps recurring, in which job, in which repositories — which is what triage routing needs. It does not tell you which individual test is flaky; that requires ingesting per-test report files (JUnit XML and friends), which is a different tool class with a different integration footprint — see the TrimCI vs. Trunk comparison for how the two approaches differ. For most teams, job-level fingerprints identify the suite, the step, and the error shape; the owning team takes it from there.
MTTR for pipelines
Failure count tells you how often things break. MTTR in CI — mean time to recovery, adapted to pipelines — tells you how long broken stays broken: the elapsed time from the first failing run to the next passing run of the same pipeline on the same branch.
Two views are worth separating:
- Red time per branch, especially the default branch. While the main branch is red, every developer merges into uncertainty, new failures hide behind the existing one, and identifying the offending change gets harder by the hour. Default-branch red time is the single most expensive interval in CI, because its cost multiplies across the whole team.
- MTTR per fingerprint. How long does a class of problem survive from first appearance to last? A fingerprint with an MTTR of three weeks is a flake that has been normalized — exactly the chronic, invisible failure ad-hoc triage cannot see.
MTTR pairs naturally with change failure rate: one measures how often a change breaks the pipeline, the other how quickly the team recovers. Both are DORA-adjacent numbers, and both fall out of run timestamps — no code access, no agent on the build machine.
One measurement caveat: resolution is bounded by data freshness. Tools that read pipeline metadata by polling the provider's API measure MTTR at the sync cadence — an hourly sync yields roughly hour-level resolution. That is entirely adequate for triage trends measured in days, and entirely inadequate for paging someone at 3 a.m. If you need minute-level incident response, that is an alerting system's job, not a failure-analysis dataset's.
Rank by cost, not frequency
Ranking failures by count optimizes for annoyance. Ranking them by cost optimizes for budget and for the team's time. The difference is easiest to see with two illustrative failures:
| Lint step failure | Integration suite failure | |
|---|---|---|
| Failures per month | 200 | 30 |
| Time to fail | 40 seconds | 25 minutes |
| Retry behavior | Rarely retried | Typically one retry per failure |
| Wasted compute per month | ~133 minutes | ~1,500 minutes |
| Engineer impact per failure | Seconds of wait, self-served fix | 25-minute wait, context switch, merge pileup |
| Chat complaints | Many | Few |
A frequency ranking says fix the lint step. A cost ranking says the integration suite wastes roughly ten times the compute and dominates the human cost too — every one of those failures parks an engineer for the better part of half an hour, and the retry doubles the compute bill for the affected run before anyone has learned anything new.
The cost of a failure has two components. Compute is the easy one: wasted minutes multiplied by your effective per-minute rate — what the provider bills for hosted runners, or the amortized cost of self-hosted ones. Human time is larger and lumpier: the wait for the re-run, the context switch, the investigation minutes, multiplied across everyone the red build blocks. The full model, with the multipliers that make human cost dominate, lives in the cost of failed builds; for a quick estimate against your own team size and failure rate, use the calculator on the landing page.
Duration is a lever in its own right: every minute shaved off a pipeline makes each future failure cheaper, which is why failure analysis and pipeline speed work compound each other.
The operational rule is simple: rank fingerprints by monthly cost — wasted compute minutes plus estimated engineer minutes — and fix from the top. The list will rarely match the list of loudest complaints, and that gap is exactly the value of doing the analysis.
A triage workflow
Analysis only pays off when it drives a recurring decision. Here is a workflow that holds up in practice — thirty minutes a week, one rotating owner (call it the CI steward), and six steps:
- Fix the window and the denominator. Look at the last 7 or 30 days — a consistent history window — and compute decisive-run failure rates per repository and pipeline. Same definitions every week, or the trend is noise.
- List the top five fingerprints by cost. Not by count. Five is enough to matter and few enough to finish.
- Bucket each one. Code, infrastructure, or flaky. If cancelled runs show up in this list, the denominator from step 1 is wrong — fix that first.
- Route with a deadline. Code failures go to the owning team's backlog with links to failing runs. Infrastructure failures become platform tickets. Flaky failures get an explicit decision — quarantine now or fix this sprint — with a date attached, because a flake without a deadline is a permanent resident.
- Check last week's items via MTTR. Anything still failing past its deadline escalates. Anything marked fixed gets verified: the fingerprint should have disappeared from the data, not just from the conversation.
- Record the decisions. One line per item. Next week starts by diffing reality against this list, which is what makes the loop self-correcting.
Three anti-patterns undo this work quietly. Auto-retry bots adopted without measuring retries formalize the retry tax instead of surfacing it. Muting the CI notification channel removes the last ambient feedback loop without replacing it. And zero-tolerance flaky-test mandates collapse within a month because they ignore cost — the sustainable version is exactly the top-five-by-cost queue above.
What metadata alone can measure
Everything in this guide runs on pipeline observability data — the metadata CI providers expose about runs, jobs, timing, and failed-job output. Mapping the questions to their data sources makes that concrete:
| Question | Metadata that answers it |
|---|---|
| Failure rate (decisive-run) | Run conclusions |
| Taxonomy bucket | Conclusions, cross-branch and cross-repo timing correlation, failed-job log tails |
| Recurring problems | Fingerprints from failed-job log tails |
| MTTR and red time | Run timestamps per branch |
| Retry tax | run_attempt on GitHub Actions (REST API docs); on GitLab CI a retry re-runs jobs inside the same pipeline (the id does not change), so extra attempts surface only through the per-job retried flag with include_retried=true (pipelines API, as of 2026-07-28), not on the run record |
| Cost ranking | Durations, failure counts, your per-minute rate |
| Queue pressure | Created-vs-started gaps — see pipeline queue time |
Notably absent from the right-hand column: source code, workflow definitions, commit diffs, and repository contents of any kind. That has a practical consequence for tool vetting — a failure analysis tool genuinely needs only read-only pipeline permissions, and a vendor asking for repository read access to do this job is asking for more than the job requires. TrimCI's own permission boundary — GitHub App scopes limited to Actions and Metadata, an HTTP-layer allowlist on the GitLab side, enforced in code — is documented in detail on the security page.
The trade-offs of metadata-only analysis are real and worth naming: no per-test granularity, no commit-level attribution, and freshness bounded by polling cadence rather than webhooks. For failure triage — knowing which problems exist, which cost the most, and whether they are getting better — those limits are comfortably acceptable. For per-test quarantine automation or real-time incident paging, pair the analysis layer with tools built for those jobs.
TrimCI automates most of this loop: it syncs run metadata from GitHub
Actions and GitLab CI (gitlab.com and self-hosted CE/EE alike, on every plan), with job-level rows
collected for failed runs only, computes
decisive-run failure rates — its decisive denominator is successes plus failures, with
cancellations counted as failures, so failure counts read as an upper bound for repositories that
auto-cancel superseded runs — plus failure fingerprints, MTTR, and a dollar-ranked per-repository
cost breakdown, and
renders the results into on-demand PDF reports — all inside the zero-code-access boundary above.
TrimCI's grouping is a fixed set of known error classes ranked by job count, not per-signature
clustering, and its MTTR is measured per pipeline and branch rather than per fingerprint — so the
cost ranking in step 2 is a manual step over its per-repository cost Pareto.
Retry tax is derived from GitHub's run_attempt, so that section is GitHub Actions-only.
Dashboards, reports, and AI recommendations are on every plan including the free one; plans differ
only in history window — 7, 30, or 60 days — quotas, and sync
cadence, and the current limits live on /pricing/.
See what your CI failures actually cost.
Connect GitHub Actions or GitLab CI with read-only access — never your source code — and get a ranked, dollar-costed fix list from your own pipeline data.