Metric · CI/CD

What is pipeline duration, and how do you measure it in Metabase?

Pipeline duration is the wall-clock time from the start of a CI run to its finish, reported as a median and a p95 per pipeline. It is the length of every developer's feedback loop — and a big input into lead time for changes. Measure it in Metabase from run history synced from Jenkins, Buildkite, CircleCI, or Azure DevOps Pipelines.

TL;DR — report the median and the p95, never the average: durations are right-skewed, and the p95 is the feedback loop your unluckiest runs actually get. Queue time is a separate metric with a separate fix.

What does pipeline duration measure?

It measures how long a developer waits between pushing and knowing. That wait compounds: slow pipelines push people into batching changes, batching inflates review size and risk, and both drag on deployment frequency and lead time for changes. Three decisions make the number trustworthy:

  • Percentiles, not averages. Durations are right-skewed — cold caches, dependency bumps, and busy runners create a long tail. The median is the typical experience; the p95 is the worst-case loop that sets how long people actually plan to wait.
  • Queue time apart from run time. Time from queued_at to started_at is a capacity problem; time from started_at to finish is a pipeline problem. Summing them hides which one you have.
  • Per pipeline, trended weekly. A fleet-wide number mixes a three-minute lint job with an hour-long release build. Segment by pipeline and watch the week-over-week drift — durations creep, they rarely jump.

What data does it need?

  • A pipeline_runs table: pipeline_name, branch, result, started_at, duration_seconds.
  • A queued_at timestamp, so queue time can be computed as started_at - queued_at and tracked on its own.
  • Run outcomes, so the duration series can be scoped to successful runs — failed runs end early and distort the distribution.

SQL patterns

Median and p95 duration by week (successful runs)PostgreSQL
SELECT
  date_trunc('week', started_at) AS week,
  COUNT(*) AS runs,
  ROUND((percentile_cont(0.5) WITHIN GROUP (
    ORDER BY duration_seconds
  ))::numeric / 60, 1) AS median_minutes,
  ROUND((percentile_cont(0.95) WITHIN GROUP (
    ORDER BY duration_seconds
  ))::numeric / 60, 1) AS p95_minutes
FROM pipeline_runs
WHERE result = 'success'
GROUP BY 1
ORDER BY 1;
Queue time vs. run time by pipeline (trailing 30 days)PostgreSQL
SELECT
  pipeline_name,
  ROUND((percentile_cont(0.5) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM started_at - queued_at)
  ))::numeric / 60, 1) AS median_queue_minutes,
  ROUND((percentile_cont(0.5) WITHIN GROUP (
    ORDER BY duration_seconds
  ))::numeric / 60, 1) AS median_run_minutes
FROM pipeline_runs
WHERE result = 'success'
  AND started_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY pipeline_name
ORDER BY median_queue_minutes DESC;
Slowest pipelines by p95 (trailing 30 days)PostgreSQL
SELECT
  pipeline_name,
  COUNT(*) AS runs,
  ROUND((percentile_cont(0.5) WITHIN GROUP (
    ORDER BY duration_seconds
  ))::numeric / 60, 1) AS median_minutes,
  ROUND((percentile_cont(0.95) WITHIN GROUP (
    ORDER BY duration_seconds
  ))::numeric / 60, 1) AS p95_minutes
FROM pipeline_runs
WHERE result = 'success'
  AND started_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY pipeline_name
HAVING COUNT(*) >= 20
ORDER BY p95_minutes DESC
LIMIT 15;

Pitfalls

Averaging durations.→ One cold-cache run at 45 minutes drags the mean of a 10-minute pipeline for the whole week, while the median doesn't move. The average describes neither the typical run nor the tail — report the median and the p95.
Mixing failed runs into the series.→ Runs that fail fast end early and flatter the number, so the metric improves whenever build success rate gets worse. Decide a convention — usually successful runs only — and apply it everywhere.
Ignoring queue time.→ When runners are saturated, jobs wait longer than they run — and no amount of build optimization fixes a capacity problem. Track queued_at to started_at as its own series before spending a sprint on caching.

Where this metric applies

Metrics

Dashboards

FAQ

Should failed runs count toward pipeline duration?
Pick a convention and hold it — the usual one is successful runs only. A run that fails in the first minute of a forty-minute pipeline ends early and flatters the number, so mixing outcomes makes the metric move when build success rate moves rather than when the pipeline actually changes speed. Filter to result = 'success', state the convention on the dashboard, and if fail-fast behavior interests you, chart failed-run duration as its own series.
What is a good pipeline duration?
Keeping PR-pipeline feedback under roughly ten minutes is a widely used bar — short enough that developers wait for the result instead of context-switching away. But the honest answer is that trend beats absolutes: a monorepo build and a microservice lint job have no business sharing a target. Watch each pipeline's median and p95 by week, and treat a creeping p95 as the early warning, since pipeline time feeds directly into lead time for changes.
Why report median and p95 instead of the average?
CI durations are right-skewed: most runs hit warm caches, while a few pay for cold caches, dependency changes, or saturated runners and take several times longer. The average lands between the two populations and describes neither. The median tells you the typical wait; the p95 tells you the worst-case feedback loop a developer regularly experiences — and it's the number worth alerting on. PostgreSQL's percentile_cont computes both in one pass, as in the SQL patterns above.
Is queue time part of pipeline duration?
Measure it separately — the fix is different. Run time (started_at to finish) reflects the work in the pipeline: caching, parallelism, test bloat. Queue time (queued_at to started_at) reflects runner capacity, and when agents are saturated it can dwarf run time while every per-step optimization does nothing. Developers experience the sum, so chart both on the CI pipeline health dashboard, but diagnose them apart.
How do you track pipeline duration in Metabase?
Sync run history from Jenkins, Buildkite, CircleCI, or Azure DevOps Pipelines into a database via their APIs or an ETL tool, keeping queued_at, started_at, and duration per run. Define median and p95 once as saved questions with percentile_cont, trend them weekly per pipeline, and put the slowest-pipelines table next to build success rate on a CI pipeline health dashboard.