trace_id alone isn't enough to join spans to logs safely. Clock skew and missing correlation IDs mean the join needs a time-window fallback.
The obvious way to correlate a log line with a span is matching trace_id. In practice, roughly 6% of log lines in a typical fleet don't carry a trace_id at all — older services, background jobs, or logs emitted just before context propagation kicks in. HelixQL's correlation join has to handle both cases.
The primary path: exact trace_id match
When both sides carry a trace_id, the join is a straightforward hash join as described elsewhere — build a hash table on spans keyed by trace_id, probe with log lines. This resolves the large majority of correlation queries and is the cheap, well-optimized path.
The fallback: time-window and process identity
For log lines without a trace_id, HelixQL can fall back to a soft join on service + pod/instance identity plus a time window bounded by the span's start and end timestamps, padded by a configurable skew tolerance (default 250ms to account for clock drift between hosts).
from traces
| where service == 'payments-api' and duration > 1.5s
| join kind=fuzzy (from logs | where service == 'payments-api')
on pod
where log.timestamp >= span.start_time - 250ms
and log.timestamp <= span.end_time + 250ms
Why fuzzy joins are opt-in
A time-window join over a busy pod can match a log line to the wrong request if two spans on the same instance overlap tightly — we measured a false-positive attach rate around 3.8% under high concurrency (dozens of requests per pod per second). Because of that, HelixQL requires `kind=fuzzy` explicitly rather than silently falling back, and every fuzzy-joined row carries a `correlation_confidence` field so downstream consumers can filter low-confidence matches out.
Cost comparison
An exact trace_id join over a 10-minute window with 2.4M spans and 8.1M logs runs in about 340ms on our benchmark cluster. The fuzzy fallback over the same window, because it can't use the trace_id hash index and must scan time-ordered log ranges per pod, takes closer to 1.9s — a real cost, which is why it's reserved for the minority of logs that genuinely lack a trace_id.
- Exact trace_id hash join handles the majority case and runs in the low hundreds of milliseconds.
- Time-window fuzzy join is opt-in, tagged with a confidence score, and roughly 5-6x slower.
- Default clock-skew tolerance is 250ms, tunable per pipeline.