Metric · Security

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

Mean time to detect (MTTD) is how long malicious activity runs before your tooling notices it — the gap between occurred_at anddetected_at, measured as a median. One honest caveat up front:occurred_at is almost always an estimate, backdated to the earliest observed activity in the kill chain, so MTTD is a lower bound on real dwell time, not a forensic truth. Measure it in Metabase from detections synced from CrowdStrike or SentinelOne.

TL;DRmedian(detected_at - occurred_at) by month and severity, plus a standing list of detections where the gap exceeded 24 hours. The median is the trend; the tail is where the work is.

What mean time to detect measures

It measures the quality of your visibility: telemetry coverage, log onboarding, and detection rules, combined into one duration. It's the segment of the response timeline that comes before MTTA (alert to human) and incident-recovery MTTR(impact to restored) — and it's the segment your on-call process can't fix, because by definition nobody has been paged yet. A shrinking MTTD means detections are firing closer to first activity; a growing tail means something in your estate has gone dark.

What data does it need?

  • A detections table with occurred_at, detected_at, severity, and technique/tactic labels.
  • The platform's estimate of activity start — CrowdStrike and SentinelOne both backdate detections to the earliest process or connection in the chain. Keep the estimation method consistent across the sync.
  • Disposition labels (true positive, benign-true-positive, false positive) so you can scope the population deliberately.
  • Asset context, so long-dwell detections can be traced to coverage gaps.

SQL patterns

Median detection hours by month and severityPostgreSQL
SELECT
  date_trunc('month', detected_at) AS month,
  severity,
  COUNT(*) AS detections,
  ROUND(
    percentile_cont(0.5) WITHIN GROUP (
      ORDER BY EXTRACT(EPOCH FROM (detected_at - occurred_at)) / 3600.0
    )::numeric, 1
  ) AS median_detection_hours
FROM detections
WHERE occurred_at IS NOT NULL
GROUP BY 1, 2
ORDER BY 1, 2;
Detections with an occurred-to-detected gap over 24 hoursPostgreSQL
SELECT
  detection_id,
  severity,
  technique,
  occurred_at,
  detected_at,
  ROUND(
    EXTRACT(EPOCH FROM (detected_at - occurred_at)) / 3600.0, 1
  ) AS gap_hours
FROM detections
WHERE detected_at - occurred_at > INTERVAL '24 hours'
  AND detected_at >= CURRENT_DATE - INTERVAL '90 days'
ORDER BY gap_hours DESC;

Pitfalls

Pretending occurred_at is precise.→ It's the platform's backdated estimate of first observed activity — anything before telemetry coverage began is invisible. Present MTTD as a lower bound with a consistent method, and say so on the dashboard itself.
Using the mean.→ One detection with a 30-day dwell drags a mean past any useful range while the typical detection fires in minutes. Usepercentile_cont(0.5) and show the tail as its own list rather than letting it distort the headline.
Conflating MTTD with MTTA.→ Detection lag is a telemetry-and-rules problem; acknowledgment lag is an on-call problem. Blending them into one "response time" hides which team owns the fix — keep MTTA as its own metric.
Excluding benign-but-real detections.→ Filtering to confirmed-malicious only shrinks the sample to the point of noise and biases it toward the incidents you noticed. Benign true positives — real activity, authorized actor — exercise the same detection pipeline, so include them and segment by disposition instead.

Where this metric applies

Metrics

Dashboards

FAQ

How accurate is occurred_at really?
It's an estimate, and you should say so wherever the metric appears. EDR platforms backdate occurred_at to the earliest observed activity in the kill chain — the first process launch or connection tied to the detection — but activity before telemetry coverage started is invisible by definition. Treat MTTD as a lower bound on real dwell time, keep the estimation method consistent so the trend is meaningful, and never present it as a precise forensic measurement.
What's the difference between MTTD and MTTA?
They cover adjacent segments of the timeline. MTTD runs from malicious activity occurring to the platform detecting it — a property of your telemetry and detection rules. MTTA runs from an alert firing to a human acknowledging it — a property of your on-call process. A breach report cares about the sum; improving them involves entirely different work, so track them separately.
Why does the dwell-time tail matter more than the median?
The median tells you what your detections usually look like; the tail tells you where an attacker could live. A handful of detections with multi-day occurred-to-detected gaps usually share a cause — a log source that wasn't onboarded, an asset outside agent coverage, a technique with no rule. That's why the 24-hour-plus list belongs next to the trend on an incident response trends dashboard, and why coverage is the first place to look when the tail grows.
How do you calculate MTTD?
Take median(detected_at - occurred_at) over detections in the window, segmented by month and severity — use percentile_cont(0.5), not AVG, because one long-dwell outlier will swamp a mean. Remember both timestamps come from the detection platform, and occurred_at is its best estimate of when activity began, so read the result as a lower bound with a consistent methodology, not ground truth.
How do you track MTTD in Metabase?
Sync detections from CrowdStrike or SentinelOne into a detections table with occurred_at, detected_at, severity, and technique. Chart median detection hours by month and severity for the trend, and keep the dwell-time tail — gaps over 24 hours — as a standing review list, annotated with the occurred_at caveat, as part of your security analytics stack.