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.
median(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
detectionstable withoccurred_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
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;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
percentile_cont(0.5) and show the tail as its own list rather than letting it distort the headline.Where this metric applies
- CrowdStrike + Metabase — Falcon detections with backdated activity timestamps
- SentinelOne + Metabase — threats with identified-at and storyline context
Related
Metrics
Dashboards
FAQ
How accurate is occurred_at really?
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?
Why does the dwell-time tail matter more than the median?
How do you calculate MTTD?
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?
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.