At scale, analytical query systems face a recurring pattern: the same time-range queries are issued over and over by dashboards, scheduled jobs, and ad-hoc exploration. When each of those queries traverses the full compute path, it wastes cycles re-deriving answers that are already known.
This post walks through how we built an interval-aware caching layer on top of Druid that dramatically reduces redundant work while keeping results correct as data arrives late.
The problem
Druid is optimized for slicing time-series data by interval. A typical dashboard query asks for metrics over the last 24 hours, the last 7 days, or a specific business window. Two queries that differ only by a few seconds of range can still require independent scans, even though 99% of the underlying segments are shared.
A naive whole-query cache fails here for two reasons:
- Query keys rarely match byte-for-byte, dashboards constantly shift their time windows forward.
- Partial invalidation is awkward: when late-arriving data lands in a segment, any cached answer that overlaps it must be recomputed.
Interval-aware keys
Instead of caching the full query result, we decompose each query into closed and open intervals. Closed intervals point at segments Druid has already sealed; these are safe to cache indefinitely. Open intervals cover the live edge of the data and are always recomputed from source.
A simplified key derivation looks like this:
def cache_keys(query, segments):
closed, open_ = [], []
for seg in segments.covering(query.interval):
if seg.is_sealed():
closed.append(("seg", seg.id, seg.version, query.agg))
else:
open_.append(seg)
return closed, open_
The key insight: once a segment is sealed, any aggregate over it is a pure function of the segment ID. That's a cache hit we can reuse across every dashboard asking the same question.
Late arrivals and correctness
Real-world pipelines don't always deliver events in order. To handle this, each cached interval records the segment version it was computed against. When a segment is replaced, due to compaction, late events, or re-ingestion, the cache entry is invalidated atomically.
on_segment_replaced(old_id, new_id):
for key in cache.scan(prefix=("seg", old_id)):
cache.delete(key)
metrics.inc("cache.invalidations")
Results
After rolling this out across the analytics fleet we observed:
- Query latency dropped by roughly 70% on cache-friendly workloads.
- Druid broker CPU usage fell by a double-digit percentage during peak dashboard hours.
- Cache correctness incidents went to zero, the interval versioning caught every late-arrival case automatically.
What's next
We're exploring adaptive TTLs on open intervals based on observed event-arrival patterns, and a shared cache layer across regions so a rebuilt dashboard in one datacenter warms the others.
If you're operating analytics systems at scale and want to chat about interval caching, reach out, we'd love to compare notes.