Metric · Software delivery

What is queue time, and how do you measure it in Metabase?

Queue time is how long work waits before anyone acts on it — a ticket sitting in "Ready for review," a build waiting for a free runner. It is usually the dominant share of lead time: items spend far longer waiting between steps than being worked on, which makes queues the highest-leverage place to look when delivery feels slow. Measure it in Metabase from status history synced from Jira or Linear, and from CI job records from Buildkite or GitHub Actions.

TL;DR — time entering a wait state → time leaving it, per state, reported as median and p90. It requires status-transition history: a sync that only stores each item's current status cannot measure waiting at all. Flow efficiency = active time ÷ total time, and it is usually lower than anyone expects.

What does queue time measure?

It measures the invisible part of delivery. Teams optimize the visible part — coding faster, reviewing faster — but an item's clock runs the whole time, and most of it runs in queues. Two kinds matter for engineering teams:

  • Workflow wait states — "Ready for review," "Ready for QA," "Blocked," "Ready to deploy." Each is a handoff where the item waits for a person. Per-state medians show exactly which handoff is the bottleneck, which is more actionable than any aggregate cycle time number.
  • CI job queue time — the gap between a job being triggered and a runner picking it up. It's invisible inside wall-clock pipeline duration, but it has a different fix: capacity, not faster tests. Rising queue time on one runner pool at predictable hours is a sizing problem, not a code problem.

The summary statistic is flow efficiency: active time divided by total elapsed time. When it's low — and it usually is — adding developers or working faster barely moves delivery, because the constraint is the waiting. Draining the worst queue raises throughput without anyone working harder.

What data does it need?

  • A status_transitions table: issue_id, state, entered_at, exited_at, and a state_category tag marking which states are wait states.
  • History, not current state. This comes from Jira's issue_changelog or Linear's issue history. A plain current-state sync overwrites transitions and cannot reconstruct waiting — if the history isn't in the sync, the metric isn't measurable.
  • For CI queues: a ci_jobs table with triggered_at, started_at, and runner_pool — Buildkite and GitHub Actions both expose the timestamps needed to derive the wait.

SQL patterns

Median & p90 wait per workflow state (90 days)PostgreSQL
SELECT
  state,
  COUNT(*) AS visits,
  percentile_cont(0.5) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (exited_at - entered_at)) / 3600.0
  ) AS median_wait_hours,
  percentile_cont(0.9) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (exited_at - entered_at)) / 3600.0
  ) AS p90_wait_hours
FROM status_transitions
WHERE state_category = 'waiting'
  AND exited_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY state
ORDER BY median_wait_hours DESC;
CI queue time per runner pool by dayPostgreSQL
SELECT
  runner_pool,
  date_trunc('day', triggered_at) AS day,
  COUNT(*) AS jobs,
  percentile_cont(0.5) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (started_at - triggered_at))
  ) AS median_queue_seconds,
  percentile_cont(0.9) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (started_at - triggered_at))
  ) AS p90_queue_seconds
FROM ci_jobs
WHERE started_at IS NOT NULL
  AND triggered_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY 1, 2
ORDER BY 1, 2;

Pitfalls

Averaging wait times.→ Queues have long tails — one ticket stuck for three weeks moves the mean more than fifty normal ones. Medians and p90s or nothing.
Measuring from a current-state sync.→ If the pipeline only stores each issue's latest status, every refresh destroys the transition history the metric depends on. Verify the sync carries the changelog before building anything on top of it.
Counting off-hours as queue time without deciding to.→ A review requested Friday evening and picked up Monday morning logs 60+ hours of wall-clock wait. That may be exactly what you want to measure — stakeholders experience wall-clock time — but decide explicitly and label the charts, or business-hours teams will look worse than follow-the-sun ones for no real reason.
Optimizing active time while queues dominate.→ If flow efficiency is 15%, halving coding time shrinks delivery by a twelfth. The per-state breakdown tells you where the time actually goes — spend improvement effort there.

Where this metric applies

Metrics

Dashboards

FAQ

How is queue time different from cycle time and lead time?
They nest. Lead time is created → done, cycle time is started → done, and queue time is the waiting buried inside both: the stretch before work starts, plus every wait state in the middle — sitting in "Ready for review" or "Ready for QA" with nobody acting on it. Subtract the queues from cycle time and what remains is active work. On most teams that remainder is a small fraction of the total, which is exactly why attacking queues moves lead time more than working faster ever could.
What is flow efficiency?
Active time divided by total elapsed time — the share of an item's life someone was actually working on it. A ticket with two days of work and eight days of waiting has 20% flow efficiency, and mature teams are often surprised to land below that. It's the summary statistic of queue time: track it on a flow efficiency dashboard to make the waiting visible, then use the per-state breakdown to find which queue to drain first.
Can I measure queue time without status history?
No. Queue time lives entirely in the transitions — when an item entered a state and when it left. A sync that only carries each issue's current status overwrites that history on every refresh, leaving nothing to measure. You need Jira's issue_changelog, Linear's issue history, or an equivalent transitions table in the sync. If your pipeline doesn't capture it, fix the pipeline first; there is no SQL workaround for data that was never stored.
Why medians and percentiles instead of averages?
Wait times are heavily right-skewed: most items clear a queue in hours while a few sit for weeks, and those few drag the mean far above what a typical item experiences. The median shows the common case, p90 shows the tail worth attacking, and the average describes neither. The same logic applies across flow metrics — cycle time and pipeline duration included — so report p50 and p90 everywhere and let averages go.
How do you track queue time in Metabase?
Sync status history from Jira or Linear into a database via an ETL tool, model a status_transitions table with entered and exited timestamps per state, and tag which states are wait states. For CI, sync job records from Buildkite or GitHub Actions and compute started_at − triggered_at per runner pool. Chart medians and p90s, and pin the per-state breakdown to a queue time dashboard.