Symptoms
- A log search alert didn't fire, but when you run its query later, the matching records are there.
- Dashboards show a dip in the last few minutes that fills in later.
- Some sources (custom logs, diagnostic settings, a busy agent) are much later than others.
Every record has a TimeGenerated (when the event happened, or when it was collected). It becomes queryable some time later. If an alert evaluates "the last 5 minutes by TimeGenerated" and the data arrives 7 minutes late, the alert never sees it.
Step 1: measure the latency
Two built-ins:
_TimeReceived: when Azure Monitor received the record.ingestion_time(): when the record was stored and became queryable.
Heartbeat
| where TimeGenerated > ago(8h)
| extend E2E = ingestion_time() - TimeGenerated,
Agent = _TimeReceived - TimeGenerated,
Pipeline = ingestion_time() - _TimeReceived
| summarize p50_e2e = percentile(E2E, 50), p95_e2e = percentile(E2E, 95),
p95_agent = percentile(Agent, 95), p95_pipeline = percentile(Pipeline, 95)
by Computer
| top 20 by p95_e2e desc
Across all tables:
union withsource = TableName *
| where TimeGenerated > ago(3h)
| extend E2E = ingestion_time() - TimeGenerated
| summarize p95 = percentile(E2E, 95), records = count() by TableName
| order by p95 desc
Typical end-to-end latency is in the range of tens of seconds to a few minutes and varies by data type. Use your own p95 values, not a guess, to design alerts.
Step 2: find where the time goes
| Large component | Likely cause | What to check |
|---|---|---|
Agent (generated → received) | Agent buffering, VM overloaded, network/proxy issues, clock skew | AMA health, VM CPU, proxy, NTP |
Pipeline (received → ingested) | DCR transformations, large records, service-side delay | Transformation complexity, record sizes, Azure status |
| Only one table late | Source-specific batching (diagnostic settings, custom logs, Logs Ingestion API batching) | Source docs; client batch size and flush interval |
| Negative or huge values | Wrong TimeGenerated from the source (clock skew, time zone, parsed field) | The source's timestamp mapping |
Check the agent side quickly:
Heartbeat
| where TimeGenerated > ago(1h)
| summarize last = max(TimeGenerated), lastIngested = max(ingestion_time()) by Computer, Category, Version
| extend lag = lastIngested - last
| order by lag desc
A clock problem shows as TimeGenerated in the future or far in the past for one machine.
Step 3: write alerts that tolerate late data
Filter on ingestion time, not event time
The key trick: select records by when they arrived, and still report by when they happened.
// Runs every 5 minutes, looks at everything that ARRIVED in the last 5 minutes
AppTraces
| where ingestion_time() > ago(5m)
| where SeverityLevel >= 3 and Message has "PaymentFailed"
| summarize failures = count(), first_seen = min(TimeGenerated) by AppRoleName
Nothing is skipped, because every record arrives exactly once in some window. The cost: the alert fires a little later than the event, never earlier.
Keep a broad TimeGenerated filter too, so the query stays efficient:
| where TimeGenerated > ago(1h)
| where ingestion_time() > ago(5m)
Or: overlap the window
If you must use TimeGenerated, make the alert's lookback longer than its frequency plus your p95 latency (for example, run every 5 minutes over the last 15), and deduplicate in the action (stateful alerts, or a correlation key).
Use metric alerts where they exist
Platform metrics (CPU, availability, queue length) arrive faster and more reliably than logs. Alert on metrics, and use logs to investigate.
Step 4: reduce latency at the source
- Diagnostic settings: send critical resource logs to the workspace directly, not via Event Hubs and a custom forwarder.
- Azure Monitor Agent: keep agents updated; avoid DCR transformations that parse huge records when a simple filter would do.
- Logs Ingestion API: flush small batches more often for alert-critical data.
- Basic and Auxiliary table plans suit cheap, high-volume data, but have query and alerting limitations; keep alert sources on Analytics tables.
Tip
Add a "latency watchdog" alert: if
p95(ingestion_time() - TimeGenerated)forHeartbeatexceeds, say, three times your normal value, notify the platform team. When monitoring is late, you want to know before an incident does.
Heartbeat
| where ingestion_time() > ago(15m)
| summarize p95 = percentile(ingestion_time() - TimeGenerated, 95)
| where p95 > 10m
Prevent it next time
- Measure latency per table once a quarter and record it next to your alert design.
- Default new log alerts to
ingestion_time()windows. - Use metric alerts for anything with a platform metric.
- Watch agent clock skew; it corrupts both latency numbers and timelines.