Skip to main content

Tuesday's Tip of the Week - Measure What Matters: MTTD, MTTR, and MTTC in Google SecOps


dnehoda
Staff
Forum|alt.badge.img+19

October 13, 2026

 

Audience: SOC managers, detection engineers, SOAR engineers


Summary: A metric is only useful if everyone measures it the same way. This briefing defines MTTD, MTTR, and MTTC with exact start and end points, maps each to the Google SecOps data source that records those timestamps, and gives the dashboard queries to calculate them.

1. Metric definitions

Metric Starts Ends Data source
MTTD (Mean Time to Detect) First malicious event (event timestamp) Alert created Detections
Alert-to-case Alert detection time Case created Cases
MTTR (Mean Time to Respond) Case created First assignment to an analyst Case history (Google calls this MTTA)
MTTC (Mean Time to Contain) Case created Containment verified Cases, via a custom field you add
Time to close (supporting) Case created Case closed Case history (Google’s samples call this MTTR)

 

Worked example, one consistent timeline:

Time What happens Metric
09:00 Malicious login occurs Start of MTTD
09:04 Detection rule creates the alert MTTD = 4 min
09:05 Case created in SOAR Alert-to-case = 1 min
09:12 Analyst assigned and starts work MTTR = 7 min
09:15 Host isolation starts  
09:20 Isolation verified MTTC = 15 min (5 min of containment execution)

The total exposure window, from the malicious login to verified containment, is 20 minutes.

2. Prerequisites

 

Without these, the queries return empty or misleading numbers:

  1. Alerts flow into SOAR cases. MTTR and MTTC are measured from case creation.
  2. Analysts are assigned to cases. MTTR uses the first ASSIGNEE_CHANGE. If a playbook auto-assigns cases, MTTR measures the automation, not the analyst.
  3. Cases are closed with a reason: Malicious, Not malicious, Maintenance, or Inconclusive. Rule quality (Section 4) depends on it.
  4. A case custom field records containment. Create a time/calendar custom field named containment time, and have the analyst set it when containment is verified, or your containment playbook if it can update custom fields. SecOps has no native “contained” timestamp.
  5. Custom rules are mapped to MITRE ATT&CK. Add a technique key to each rule’s meta: section, for example technique = "T1548,T1134.001". Without it, custom rules don’t count toward coverage.

3. Calculating the core metrics

 

MTTD: first event to alert

stage per_alert {  detection.detection.alert_state = "ALERTING"  $detection_id = detection.id  match:    $detection_id  outcome:    $first_event_time = min(detection.collection_elements.references.event.metadata.event_timestamp.seconds)    $TTD = max(detection.created_time.seconds) - $first_event_time  condition:    $first_event_time > 0}outcome:  $alert_count = count($per_alert.detection_id)  $MTTD_minutes = math.round(avg($per_alert.TTD) / 60, 2)
  • How it works: For each alerting detection, the query takes the earliest event behind it as the time the threat occurred. For a multi-event rule, that’s the first event in the match window, not the last.
  • What drives MTTD:
    • Ingestion latency: check Health Hub and the Week 17 dashboards.
    • Rule run frequency: real-time rules run sooner than hourly or daily ones.
    • Match window length: a rule with a 1-hour window can’t fire until enough events arrive within that window.
  • Exclude retrohunts: Retrohunt and reprocessing detections are created long after the events, so they inflate MTTD. The Detections data source flags these runs in detection.detection_timing_details. Confirm the values in your tenant and exclude them.
  • Watch for clock skew: The event timestamp comes from the source system. If a source’s clock is wrong, its MTTD will be wrong too.

Alert-to-case

$case_create_time = case.create_time.seconds$alert_time = case.alerts.metadata.detection_time.seconds$alert_time > 0outcome:  $avg_minutes = math.round(window.avg($case_create_time - $alert_time) / 60, 2)

$alert_time > 0 is required. Google documents that case.alerts.metadata.detection_time.seconds defaults to 1970-01-01 when it’s empty, and one empty value would distort the average badly. This number should be close to zero. If it isn’t, alerts are queuing before they reach SOAR.

MTTR: case created to first assignment

stage stage1 {$case_id = case_history.case_response_platform_info.case_idmatch:  $case_idoutcome:  $case_assign_time = min(if(case_history.case_activity = "ASSIGNEE_CHANGE", case_history.event_time.seconds, 9999999999999999))  $status = array_distinct(case_history.case_activity)  $TTA = $case_assign_time - min(case_history.event_time.seconds)condition:  arrays.contains($status, "CREATE_CASE") and arrays.contains($status, "ASSIGNEE_CHANGE")}outcome:  $case_count = count($stage1.case_id)  $MTTA = (math.round((avg($stage1.TTA)/60)))

High MTTR usually points to analyst overload, poor prioritization, or thin staffing on certain shifts. Auto-enrichment playbooks reduce the work analysts do after picking up a case. They only lower this number if they also speed up assignment.

MTTC: case created to containment verified

$created = case.create_time.seconds$contained = case.custom_fields["containment time"].time_val.seconds$contained > 0outcome:  $MTTC_minutes = math.round(window.avg($contained - $created) / 60, 2)

Only cases where containment was recorded are counted. If you also want containment execution time (start to verified, the original tip’s definition), add a second custom field, containment start, and subtract it instead of $created.

Time to close (supporting metric)

stage stage1 {$case_id = case_history.case_response_platform_info.case_idmatch:  $case_idoutcome:  $case_close_time = max(if(case_history.case_activity = "CLOSE_CASE", case_history.event_time.seconds, 0))  $status = array_distinct(case_history.case_activity)  $TTC = $case_close_time - min(case_history.event_time.seconds)condition:  arrays.contains($status, "CREATE_CASE") and arrays.contains($status, "CLOSE_CASE")}outcome:  $case_count = count($stage1.case_id)  $MTTC = (math.round(avg($stage1.TTC)/60))

Google’s sample names this output $MTTC, but it measures time to close, not time to contain. Label the chart accordingly.

Means hide outliers. One case that took three days can double the average. Alongside each mean, review the slowest cases.

 

4. Quarterly review

 

Detections, cases, case history, and playbooks data all go back 365 days. To trend any metric, run its query once for each quarter’s time range and compare the results.

Rule quality: true positive rate by rule

case.status = "CLOSED"$rule_name = case.alerts.metadata.detection.rule_name$rule_name != ""match:  $rule_nameoutcome:  $closed_alerts = count(case.name)  $malicious = sum(if(case.closure_details.reason = "MALICIOUS", 1, 0))  $tp_rate_pct = math.round(($malicious / $closed_alerts) * 100, 2)order:  $tp_rate_pct asc

Rules at the top of the list need tuning first. Pick a threshold your team agrees on (the original tip suggests 50%) and review any rule below it. Cases closed as Maintenance count against the rule here. Exclude them if your team uses that reason for planned activity.

Playbook effectiveness: containment action outcomes

playbook.action.action = /isolate|contain|block|disable/ nocase$action = playbook.action.actionmatch:  $actionoutcome:  $total = count(playbook.action.name)  $completed = sum(if(playbook.action.status = "COMPLETED", 1, 0))  $faulted = sum(if(playbook.action.status = "FAULTED", 1, 0))  $timed_out = sum(if(playbook.action.status = "PENDING_ACTION_TIMEOUT" or playbook.action.status = "UNHANDLED_TIMEOUT", 1, 0))order:  $total desc

Adjust the regex to match your containment action names.

  • Many faults: the integration or its permissions are broken.
  • Many approval timeouts: analysts aren’t responding to approval requests. Either the trigger is too broad or approvals are going to the wrong people.
  • “Dismissed” decisions: SecOps doesn’t record an explicit “dismissed” decision. If you need that number, record the approve or deny choice in the playbook, for example as a case tag.

Coverage gaps: MITRE ATT&CK Matrix
Go to Detection → Rules & Detections → MITRE ATT&CK Matrix. Look for technique cards that show 0 rules but list available log types. Those are techniques where you already have the data and only need a rule, so build those first. Use the Rule type, Live status, and Alerting status filters to count only rules that are actually live, and Export the view as JSON to use in the MITRE ATT&CK Navigator.

Trend analysis: Compare MTTD, MTTR, and MTTC quarter over quarter using the same queries and the same definitions. If they’re flat or getting worse while alert volume grows, the problem is process or tooling, not effort.

 

5. Using metrics to justify investment

 

Before-and-after comparisons only hold up if you measure both periods with the same definition and query. For example:

“After deploying auto-enrichment and auto-assignment playbooks in Q2, MTTR (case created → first assignment) dropped from 45 minutes to 8 minutes, measured with the same case history query across Q1 and Q2.”

Back it up with automation coverage. Google’s Automation distribution by environment sample groups playbook runs by playbook.metadata.automatic, which shows how much of your response is now automated.

These metrics connect detection engineering (MTTD, rule quality, coverage), SOAR automation (MTTR, MTTC, playbook outcomes), and dashboards into one measurable picture of how the SOC is performing.

 

6. Action items

  • Confirm alerts flow into SOAR cases and analysts are assigned to them
  • Make a close reason mandatory when closing cases
  • Create the containment time custom field and record it on every contained case
  • Add technique = "T…" to the meta: section of every custom rule
  • Build the MTTD, alert-to-case, MTTR, MTTC, and time-to-close charts
  • Hold a quarterly review covering rule TP rate, containment action outcomes, and ATT&CK coverage