Metric · Security

What is mean time to remediate (MTTR), and how do you measure it in Metabase?

Mean time to remediate (MTTR) is how long it takes to fix a vulnerability once it's been detected — measured, despite the name, as a median, in days, segmented by severity. It's the flow metric that explains whether your findings backlog is draining or silting up. Measure it in Metabase from findings synced from Tenable, Snyk, Wiz, or Semgrep. (Not the incident-recovery MTTR — same acronym, different clock.)

TL;DRmedian(resolved_at - first_seen_at) over closed findings, by severity. Use percentile_cont(0.5), not AVG: fix times are right-skewed, and one 400-day finding shouldn't define the metric.

What mean time to remediate measures

It measures the speed of your fix pipeline: triage, ticketing, patching, and verification, end to end. Where open critical vulnerabilities tells you how big the backlog is, remediation time tells you whether it's moving. Segmented by severity it becomes a commitment you can check — criticals in days, highs in weeks — and the direct input to SLA compliance.

What data does it need?

  • A vulnerability_findings table with first_seen_at, resolved_at, status, and normalized severity.
  • First-seen timestamps carried through from the scanner — not overwritten by your pipeline's import time.
  • Resolution verified by a rescan, not just a ticket closed — otherwise you're measuring ticket hygiene.
  • Asset or team attribution if you want per-team medians instead of one org-wide number.

SQL patterns

Median days to remediate by severity (last 180 days)PostgreSQL
SELECT
  severity,
  COUNT(*) AS findings_closed,
  ROUND(
    percentile_cont(0.5) WITHIN GROUP (
      ORDER BY EXTRACT(EPOCH FROM (resolved_at - first_seen_at)) / 86400.0
    )::numeric, 1
  ) AS median_days_to_remediate
FROM vulnerability_findings
WHERE status = 'resolved'
  AND resolved_at >= CURRENT_DATE - INTERVAL '180 days'
GROUP BY severity
ORDER BY median_days_to_remediate;
Remediation time distribution by month closed (p50 and p90)PostgreSQL
SELECT
  date_trunc('month', resolved_at) AS month,
  COUNT(*) AS findings_closed,
  ROUND(
    percentile_cont(0.5) WITHIN GROUP (ORDER BY days_open)::numeric, 1
  ) AS p50_days,
  ROUND(
    percentile_cont(0.9) WITHIN GROUP (ORDER BY days_open)::numeric, 1
  ) AS p90_days
FROM (
  SELECT
    resolved_at,
    EXTRACT(EPOCH FROM (resolved_at - first_seen_at)) / 86400.0 AS days_open
  FROM vulnerability_findings
  WHERE status = 'resolved'
) closed
GROUP BY 1
ORDER BY 1;

Pitfalls

Using the mean.→ Remediation times are right-skewed: most findings close in days, a few take a year. The mean chases the outliers; the median describes the process. Report p50 with p90 alongside so the tail stays visible.
Only counting closed findings.→ A median over closed findings is survivorship-biased — the finding that's been open 400 days never enters it, so the metric improves while the backlog rots. Always show the aging open backlog next to the closed-only median.
Starting the clock at scan import.→ If first_seen_at gets stamped when your ETL runs rather than when the scanner first observed the finding, every number is flattered by your sync lag. Carry the scanner's first-seen timestamp through untouched.
Not segmenting by severity.→ A blended MTTR mixes criticals patched in days with lows deferred for months and describes neither. The severity split is the metric — a single org-wide number is trivia.

Where this metric applies

Metrics

Dashboards

FAQ

Is this the same as MTTR for incidents?
No — same acronym, different clock. Incident-recovery MTTR measures hours from an outage starting to service restored; remediation MTTR measures days from a vulnerability being detected to its fix being verified. They live in different tools, run on different timescales, and answer different questions. If a dashboard shows "MTTR" without saying which, ask — mixing them makes both meaningless.
Why use the median instead of the mean?
Because remediation times are heavily right-skewed. One finding stuck for 400 days behind a legacy system drags the mean far above what your process typically delivers, and a burst of quick patches then "improves" it without anything changing. The median — percentile_cont(0.5) in PostgreSQL — describes the typical finding; add p90 to keep the slow tail visible instead of averaged away.
When does the remediation clock start?
At first_seen_at — the first time any scanner observed the finding — not at the timestamp your pipeline imported it. Import time flatters you by however long the sync lag is, and the attacker's window opened at detection, not at your ETL run. Tenable and Wiz both expose first-seen timestamps; carry them through the sync untouched.
How do you calculate mean time to remediate?
Take median(resolved_at - first_seen_at) over findings closed in the window, segmented by severity — despite the name, use the median, not the mean. Critical findings should show a very different number than lows; a single blended figure hides exactly the split that matters. Pair it with the still-open backlog from open critical vulnerabilities so closed-only survivorship bias can't flatter the trend.
How do you track mean time to remediate in Metabase?
Sync findings from Tenable, Snyk, Wiz, or Semgrep into a vulnerability_findings table with first_seen_at, resolved_at, and severity. Chart median days by severity for the headline, p50/p90 by close month for the trend, and put both on a vulnerability management dashboard next to your SLA compliance figures.