← Back to all questions
StaffAnalyticsInfrastructure

Design a Real-Time Event Analytics System

A full Staff-level walkthrough of a Scuba-style interactive analytics engine that makes billions of events per day queryable within seconds — trading memory cost against fidelity through in-memory columnar storage, priority sampling, and scatter-gather execution with partition pruning. It spans ingestion, schema evolution, and multi-datacenter fan-out, plus the cross-team strangler-fig migration that makes it a Staff problem. Follow it top to bottom, or jump to any step.

Level
Staff
Category
Analytics · Infrastructure
Interview time
60 min
100% free · No login required
WHAT THIS QUESTION TESTS
·In-memory columnar store: dictionary encoding and bit-packing for ~15x compression
·Priority sampling: 100% of errors and tail-latency events, sampled normal traffic
·Scatter-gather with partition pruning so fan-out tracks the time range, not the fleet size
·30-second emit-to-queryable freshness, with query-time sample correction and error bars
★ STAFF-LEVEL SIGNALS
Frames sampling as the lever between a ~$4M and ~$25M monthly bill, then designs per-table config around it
Compares row vs pure-columnar vs hybrid and commits to in-memory columnar for the hot tier
Local-first multi-DC: each datacenter self-serves its events, cross-DC fan-out only for global aggregates
Strangler-fig migration off the existing system with dual-ingest validation across six teams
0

Scope & ambiguity

Framing statement:

“Meta needs to query billions of events with sub-second latency for debugging, monitoring, and product analytics. The key challenge is interactive querying at petabyte scale — engineers must be able to slice-and-dice events by any dimension in real-time, unlike traditional batch analytics that take minutes. I’ll focus on the ingestion pipeline, in-memory columnar storage, and query engine.”

Phased delivery:

Phase
Timeline
Scope
Phase 1
0-3 months
Core ingestion via Kafka, in-memory columnar store for top 10 services, basic scatter-gather query engine, single datacenter, 1-day retention
Phase 2
3-9 months
All services onboarded (1000+ event tables), multi-DC (US-East, US-West, EU), dashboard infrastructure, 7-day retention, priority sampling, schema evolution
Phase 3
9-18 months
Automated anomaly detection, ML root cause analysis, incident management integration, predictive alerting, self-serve event table creation

Team ownership:

Team
Ownership
Ingestion Pipeline Team
Kafka consumers, event validation, sampling decisions, columnar conversion
Storage Engine Team
In-memory columnar store, partitioning, compression, memory management, eviction
Query Engine Team
Query coordinator, scatter-gather execution, partial aggregation, query optimization
Dashboard & Visualization Team
Saved queries, dashboard builder, auto-refresh, embeddable widgets, incident war room UI
Schema & Metadata Team
Schema registry, schema evolution, table configuration, sampling config
Observability Infrastructure Team
Alerting integration, anomaly detection pipeline, SLA monitoring for Scuba itself

Key ambiguity I’d challenge:

“When we say ’real-time,’ do we mean all 10B daily events queryable within 30 seconds? Or is sampling acceptable? Full fidelity needs ~2.5PB in-memory; priority sampling at 10% average brings this to ~250TB. I want to clarify freshness and fidelity expectations per event category.”
1

Requirements

Functional Requirements (Phase 1)

  • FR1: Ingest structured events from any Meta service via Kafka with schema validation
  • FR2: Events queryable within 30 seconds of emission (data freshness guarantee)
  • FR3: Interactive SQL-like queries with GROUP BY, WHERE on any dimension, percentiles, count/sum/avg
  • FR4: Priority sampling — 100% errors/exceptions, 100% p99+ latency events, configurable rate for normal events
  • FR5: Time-series drill-down (1-min granularity last hour, 5-min last day, 1-hour last week)
  • FR6: Dashboard infrastructure — saved queries, auto-refreshing, shareable, embeddable in incident war rooms

Non-Functional Requirements

Requirement
Target
Event ingestion rate
10B+ events/day, ~100K events/sec sustained, 300K peak
Query latency
p50 < 1s, p99 < 5s for interactive queries
Data freshness
Events queryable within 30 seconds of emission
Retention (hot tier)
1-7 days (older data moves to Hive/data warehouse)
Query patterns
GROUP BY any dimension, time-series aggregations, percentiles
Concurrent queries
1000+ engineers querying simultaneously
Availability
99.9% (analytics system, not on critical serving path)
Query accuracy
Within 5% of true value for sampled data (with error bars displayed)
Schema evolution
Backward-compatible changes without downtime
Multi-DC
Active-active in 3+ datacenters, each DC answers queries from local data

Organizational Context

  • Existing system: Scuba already exists — this is evolution/re-architecture, not greenfield
  • Migration: Strangler fig pattern from current Scuba to next-gen system
  • Meta stack: Kafka, Thrift, Scribe, MySQL, Memcached, Hive/HDFS, Spark, Presto
  • Users: 1000+ internal teams. Culture: move fast, data-driven debugging during SEVs, <1s query expectation
  • Timeline: Phase 1 in 3 months with Ingestion + Storage + Query teams (~12 engineers)

Out of Scope (Explicitly)

  • Batch analytics (Hive/Spark pipelines — separate team)
  • Time-series monitoring metrics (ODS/Gorilla — different access pattern)
  • Full-text log search (LogDevice — different storage model)
  • ML training pipelines (FBLearner — different latency/throughput tradeoffs)
2

Back-of-envelope estimation

Scale Numbers

Metric
Value
Daily events
10B+
Peak events/sec
300K
Sustained events/sec
100K
Unique event tables
10,000+
Avg event size
500 bytes (20-50 dimensions typical, up to 200)
Concurrent queries
1,000+ at peak
Active dashboards
50,000+ (refreshing every 30s-5min)
Dimensions per event
20-50 typical, up to 200

Storage Estimation

Data
Calculation
Result
Raw daily ingestion
10B x 500B
5TB/day
After sampling (10% avg)
5TB x 0.10
500GB/day
7-day in-memory retention
500GB x 7
3.5TB raw
With columnar compression (15x)
3.5TB / 15
~230GB effective
Memory budget
1000 servers x 256GB
256TB capacity
Hive archive (full data, 90 days)
5TB x 90
450TB

Cost Estimation

Resource
Calculation
Monthly Cost
Memory servers (1000 x 256GB)
High-memory instances
~$3M
Kafka cluster (ingestion)
Multi-DC, replication factor 3
~$500K
Query coordinators (100 servers)
Stateless compute
~$150K
Hive/HDFS archive
Cold storage
~$200K
Network (cross-DC replication)
Kafka MirrorMaker traffic
~$300K
Total
~$4.15M/month

Architectural implication: Memory fleet = 72% of total cost. Sampling strategy directly impacts total cost. Reducing average sample rate from 10% to 5% saves ~$1.5M/month. Priority sampling is the critical cost lever — we need per-table sampling configuration to optimize cost vs. fidelity tradeoffs.

3

API design

External API (Engineer-Facing)

# Query
POST /api/v1/query -- SQL-like query (table, columns, aggregations, filters, group_by)
 
# Dashboards
POST /api/v1/dashboards -- Create dashboard
GET /api/v1/dashboards/{id} -- Get dashboard with live data
PUT /api/v1/dashboards/{id} -- Update dashboard
DELETE /api/v1/dashboards/{id} -- Delete dashboard
 
# Event Table Management
POST /api/v1/tables -- Create event table (schema definition)
GET /api/v1/tables/{table}/schema -- Get current schema
PUT /api/v1/tables/{table}/schema -- Evolve schema (backward-compatible only)
GET /api/v1/tables/{table}/stats -- Table stats (row count, memory usage, sample rate)

Inter-Service Contracts (Thrift — Meta convention)

SERVICE: EventIngestionService
OWNER: Ingestion Pipeline Team
PROTOCOL: Thrift
 
IngestBatch(IngestBatchRequest) returns (IngestBatchResponse)
- IngestBatchRequest: { table_name, events[], schema_version }
- Events arrive via Kafka topic: scuba.ingested, partitioned by table_name
- Latency SLA: p99 < 50ms per batch
- Availability SLA: 99.99%
SERVICE: QueryService
OWNER: Query Engine Team
PROTOCOL: Thrift
 
ExecuteQuery(QueryRequest) returns (QueryResponse)
- QueryRequest: { table, columns[], aggregations[], filters[], group_by[], time_range }
- QueryResponse: { rows[], metadata { sample_rate, error_margin, partitions_scanned } }
- Latency SLA: p99 < 5s interactive, p99 < 30s complex aggregations
- Availability SLA: 99.9%
- Accuracy SLA: within 5% of true value for sampled data
SERVICE: SchemaRegistryService
OWNER: Schema & Metadata Team
PROTOCOL: Thrift
 
GetSchema(table_name, version) returns (SchemaResponse)
RegisterSchema(table_name, schema) returns (RegisterResponse)
GetSamplingConfig(table_name) returns (SamplingConfig)
- Latency SLA: p99 < 10ms (heavily cached)
- Availability SLA: 99.99%

API Governance

  • Schema registry: Thrift IDL for all inter-service contracts
  • Evolution: Backward-compatible only — additive fields, no removal or type changes
  • Rate limiting: Per-user: 100 queries/min, per-dashboard: 60 refreshes/min
  • Query cost estimation: Dry-run mode returns estimated partitions, rows, and latency before execution
4

Data model

Core Entities

EventTable: table_id (PK), table_name (unique), owner_team, schema_version,
sample_rate_config (JSON), retention_days, created_at
 
EventSchema: schema_id (PK), table_id (FK), version, is_active,
columns [{ name, type (INT64|DOUBLE|STRING|BOOL), encoding, dictionary_size }]
 
EventPartition: partition_id (PK), table_id, time_bucket (1-min), server_id,
row_count, memory_bytes, sample_rate_applied, schema_version
 
SavedQuery: query_id (PK), creator_id, query_text, table_id (FK)
Dashboard: dashboard_id (PK), owner_id, title, queries (JSON), refresh_interval_sec

Data Ownership Boundaries

Data
Owner Service
Access Pattern for Others
Event partitions (hot)
Storage Engine
Via QueryService scatter-gather (sync Thrift)
Table schemas
SchemaRegistryService
Sync Thrift, cached on ingesters and query servers (60s TTL)
Sampling config
SchemaRegistryService
Per-table config, cached on ingesters (60s TTL)
Dashboard/query definitions
Dashboard Service
MySQL + Memcached (sync reads, async writes)
Audit log
Kafka → Hive
Async batch pipeline

Storage Technology Choices

Data
Technology
Rationale
Alternative Considered
Hot events (0-7d)
Custom in-memory columnar
Sub-second aggregation over billions of rows, dictionary encoding, bit-packing
Redis (rejected: no columnar scan/aggregation support)
Cold events (7d+)
Hive/HDFS Parquet
Cheap columnar storage, Spark/Presto queryable
On-disk columnar warm tier (future Phase 3)
Metadata (tables, schemas)
MySQL + Memcached
Meta standard stack, relational queries, well-understood
ZooKeeper (rejected: not relational, poor for structured metadata)
Dashboards/saved queries
MySQL
Moderate write rate, relational joins, Meta convention
TAO (rejected: not graph data, simpler relational model fits)
Audit/change log
Kafka → Hive
Async, immutable, batch analytics
Scribe (acceptable alternative, same outcome)

Data Lifecycle

Tier
Retention
Storage
Latency
Notes
HOT
0-7 days
In-memory columnar
Sub-second
Error events: 100% fidelity, 30-day hot
COLD
30+ days
Hive/HDFS Parquet
Minutes (Spark/Presto)
Full un-sampled data, 90-day retention
5

High-level architecture

Architecture Diagram

[Application Services (1000+)]
|
[Scribe / Kafka Producers] (Thrift-serialized events)
|
[Kafka Cluster] (partitioned by table_name, RF=3)
|
+-----+-----+
| |
[Scuba Ingester Fleet] [Hive/Spark Batch Pipeline]
| (validate, sample, (cold path, full data)
| dict-encode, columnarize)
|
[In-Memory Columnar Store] (1000 servers, table_name + time_bucket)
|
[Query Coordinator] (stateless, 100 servers: parse → prune → scatter → gather)
|
[Dashboard Service / Query API]
|
[Engineer Browser / Incident War Room]
 
Cross-DC: [DC-East] ←MirrorMaker→ [DC-West] ←MirrorMaker→ [DC-EU]
Each DC has full ingestion + storage + query stack

Service Boundaries with Ownership

Service
Team
Data Store
Scaling
Consistency
SLA
Kafka Ingestion
Ingestion Pipeline
Kafka topics
Horizontal (add partitions)
At-least-once delivery
99.99%
Scuba Ingester
Ingestion Pipeline
— (stateless, writes to columnar store)
Horizontal (1 ingester per Kafka partition)
Eventual (30s freshness)
p99 < 50ms per batch
Columnar Store
Storage Engine
In-memory columnar segments
Horizontal (add servers, rebalance tables)
Strong per-partition
p99 < 100ms scan
Query Coordinator
Query Engine
— (stateless)
Horizontal, N+2 redundancy
Best-effort (partial results on failure)
p99 < 5s interactive
Schema Registry
Schema & Metadata
MySQL + Memcached
Vertical + read replicas
Strong (MySQL leader)
p99 < 10ms
Dashboard Service
Dashboard & Viz
MySQL + Memcached
Horizontal (stateless app tier)
Eventual (cached queries)
p99 < 200ms

Sync vs Async Communication

Communication Path
Type
Rationale
App → Kafka
Async (fire-and-forget)
Producers must not block on analytics pipeline
Kafka → Ingester
Async (consumer pull)
Backpressure via consumer lag, decoupled from producer rate
Ingester → Columnar Store
Sync (local write, co-located)
Ingester and store server are on same machine
Query Coordinator → Store servers
Sync (Thrift scatter)
User is waiting for query results
Kafka → Hive
Async (batch hourly ETL)
Cold path, no latency requirement
Cross-DC replication
Async (Kafka MirrorMaker)
Each DC independent, eventual consistency across DCs

Failure Domain Boundaries

Failure Domain
Impact
Mitigation
FD1: Ingestion (Kafka + Ingesters)
New events stop flowing, existing data queryable
Kafka RF=3, ingester auto-restart, 24h Kafka retention buffer
FD2: Columnar Store (server crash)
Partial query results (missing partitions)
Replicate partitions on 2 servers, return with "partial" warning
FD3: Query Coordinator
Cannot execute queries, dashboards stale
Stateless, N+2 redundancy, any coordinator handles any query
FD4: Cross-DC Replication
Cannot run global queries
Each DC self-sufficient for local services
6

Deep dives

WHERE STAFF IS WON

Deep Dive 1: In-Memory Columnar vs Row Store

Dimension
Row Store (MySQL)
In-Memory Columnar
Hybrid (Columnar + Disk Overflow)
Query latency (aggregation over 1B rows)
Minutes (full table scan)
Sub-second (only read needed columns)
Seconds (hot in-memory, cold on disk)
Write throughput
50K rows/sec per server
200K rows/sec per server (append-only)
150K rows/sec
Memory efficiency
~500B per row (full row)
~30B per row (compressed columnar)
~30B hot, ~5B cold
Compression ratio
2-3x
10-20x (dictionary + bit-packing)
10-20x
Ad-hoc query flexibility
Full SQL, but slow
Any column filter/aggregate, fast
Same, variable latency
Cost at 10B events/day
Infeasible (PBs of RAM)
$3M/mo (with sampling)
$2M/mo (less RAM needed)

Recommendation: Pure in-memory columnar for hot tier. The 10-20x compression advantage and sub-second scan performance make it the only viable option for interactive analytics at this scale. Hybrid is a Phase 3 optimization.

In-Memory Columnar Storage (detailed):

Storage layout (each 1-minute time partition):
Column "timestamp": [1705000001, 1705000002, ...]
Column "country": [0, 0, 1, ...] (dictionary-encoded: "US"=0, "UK"=1, "DE"=2)
Column "latency_ms": [50, 120, 30, ...]
 
Encoding techniques:
- Dictionary encoding: string → integer lookup (low-cardinality: 1-4 bytes/value)
- Bit-packing: country (200 values) → 8 bits, status_code (5 values) → 3 bits
- Compression ratios: strings 20-50x, integers 5-10x, overall avg 15x
 
Memory budget: 1000 servers × 256GB = 256TB capacity → ~3.5PB logical data after compression

Deep Dive 2: Sampling Strategy

Dimension
No Sampling
Uniform Random
Priority/Weighted
Head-Based Trace
Memory required (7 days)
~2.5PB (infeasible)
~250TB (10% rate)
~250TB (variable rate)
~300TB (trace-complete)
Error event coverage
100%
10% (miss 90% of errors)
100% (priority rule)
100% (if error triggers trace)
Statistical accuracy (counts)
Exact
+/-3% at 10% rate
+/-3% for normal, exact for priority
Depends on trace sampling rate
Tail latency debugging
All events available
Miss 90% of slow requests
100% of p99+ events
Complete trace for sampled requests
Implementation complexity
None
Low (random number)
Medium (rule engine)
High (distributed trace coordination)
Cost (monthly)
~$25M+
~$3M
~$3M
~$3.5M

Recommendation: Priority sampling with head-based trace preservation. Priority rules ensure 100% coverage of the events that matter most (errors, slow requests), while random sampling of normal events keeps costs manageable. Head-based trace preservation means if any event in a trace is kept, all events in that trace are kept — critical for debugging request flows.

Sampling decision flow (at ingester):

1. Ingester receives event batch from Kafka
2. Look up table's sampling config (cached from SchemaRegistry, 60s TTL)
3. For each event:
a. Check priority rules: error? → keep 100%. P99+ latency? → keep 100%. SEV-affected? → keep 100%.
b. Else → deterministic hash: hash(trace_id) < threshold
(Deterministic on trace_id ensures all events in same trace get same decision)
c. Tag event with applied sample_rate for query-time correction
4. Write kept events to columnar store
 
Query-time correction:
- Counts: multiply by 1/sample_rate to estimate true count
- Error bars: +/- 1.96 * sqrt(count * (1-sample_rate) / sample_rate) for 95% CI
- For 100%-sampled categories (errors), exact values — no error bars

Deep Dive 3: Query Execution

Dimension
Scatter-Gather
Pre-Aggregation
Materialized Views
Query flexibility
Any ad-hoc query
Only pre-defined aggregation keys
Only pre-defined view definitions
Latency (simple query)
p50 < 1s
p50 < 100ms (cache hit)
p50 < 50ms (pre-computed)
Latency (complex query)
p50 < 5s
Not applicable (falls back to scatter)
Not applicable
Storage overhead
None (query raw data)
5-10% additional memory
10-20% additional memory
Data freshness
Real-time (latest partition)
1-5 min delay (aggregation lag)
1-5 min delay
Implementation complexity
Medium
Medium
High (view maintenance)

Recommendation: Scatter-gather as primary execution model, with optional pre-aggregation for popular dashboards. Scatter-gather preserves full ad-hoc flexibility (Scuba’s key value proposition). Pre-aggregation is a Phase 2 optimization for dashboards that refresh every 30s and always run the same query.

Query execution engine:

Example: SELECT country, percentile(latency_ms, 99), count(*)
FROM web_requests WHERE service='feed_ranking' AND timestamp > now()-1h GROUP BY country
 
Execution:
1. PARSE → AST → identify table, time range, predicates, aggregations
2. PRUNE → time range (1h) = 60 one-minute partitions → lookup partition map → scatter to 15 servers (not 1000)
3. SCATTER → parallel Thrift RPCs to relevant storage servers
4. LOCAL SCAN → column pruning + predicate pushdown + vectorized exec (1024 rows/batch, SIMD)
→ compute partial aggregates (counts, t-digest sketches for percentiles)
5. GATHER → merge partials, apply sample rate correction, return with metadata
 
Partition pruning: floor(start/60s) to ceil(end/60s) → lookup server assignments → deduplicate
Result: fan-out proportional to time range, not cluster size
 
Optimizations: predicate pushdown, column pruning, vectorized SIMD execution,
pre-aggregated rollups for dashboards, result caching (30s TTL), 30s query timeout

Deep Dive 4: Schema Evolution

Dimension
Schema-on-Read
Strict Versioning
Backward-Compatible Evolution
Flexibility
Maximum (any structure)
Rigid (version lock)
Moderate (additive only)
Query performance
Poor (runtime type resolution)
Best (compile-time optimization)
Good (known types, NULL handling)
Data quality
Low (no validation)
High (strict validation)
High (validated at ingest)
Migration effort
None
High (coordinated schema change)
Low (additive, no migration)
Developer experience
Easy to start, hard to debug
Hard to change, reliable
Easy to evolve, predictable

Recommendation: Backward-compatible evolution (additive changes only). This balances flexibility with data quality. New columns can be added freely; old events have NULL for new columns. Column type changes require a new column name (e.g., latency_ms INT → latency_us INT).

Schema evolution protocol:

Rules: ADD column → allowed (old events NULL). REMOVE → soft-delete only. TYPE CHANGE → new column name.
 
Flow:
1. Team submits change → SchemaRegistry validates compatibility
2. New version (N+1) registered → pushed to ingesters via Kafka config topic (scuba.schema_updates)
3. Ingesters pick up within 60s → new events use version N+1
4. Query engine handles mixed-version partitions: old partitions return NULL for new columns
5. No downtime, no data migration, no coordinated deploy
 
Edge case: high-cardinality column → SchemaRegistry warns, team chooses RAW encoding (not dictionary)

Deep Dive 5: Multi-Datacenter Deployment

Dimension
Single DC (DR only)
Each DC Local Events + Cross-DC Fan-Out
Replicate All Events to All DCs
Query latency (local)
p50 < 1s
p50 < 1s
p50 < 1s
Query latency (global)
p50 < 1s (all data local)
p50 < 3s (cross-DC fan-out)
p50 < 1s (all data local)
Storage cost
1x
1x (each DC stores local data)
3x (full replication)
Network cost
Low
Medium (cross-DC queries)
High (replicate all events)
Availability during DC outage
Failover (minutes)
Other DCs unaffected, lose that DC's data
Other DCs have all data
Incident debugging
Must route to correct DC
Local debug fast, global query slower
Debug from any DC

Recommendation: Each DC stores local events, cross-DC fan-out for global queries. This is the best cost/latency tradeoff. During incidents, engineers are typically debugging a specific DC’s services — local queries are fast (< 1s). For global aggregations (e.g., “total error rate worldwide”), the query coordinator fans out to all 3 DCs.

Multi-DC architecture:

DC-East (Virginia, ~40% traffic) | DC-West (Oregon, ~35%) | DC-EU (Dublin, ~25%)
Each DC: independent Kafka + ingesters + columnar store + query coordinators
 
Global query: coordinator fans out to all 3 DCs → each runs local scatter-gather → merge
Latency: p50 < 3s (slowest DC dominates)
Local query (dc_filter='us-east'): scatter to local DC only → p50 < 1s
During incidents: debug local DC first → fast iteration, no cross-DC dependency
 
Kafka MirrorMaker: global tables (e.g., "global_errors") mirrored to all DCs
Most tables DC-local only. Replication lag: p50 < 5s, p99 < 30s
7

Multi-team rollout

Migration Strategy: Scuba v1 → v2 (Strangler Fig)

Scuba v1 exists and is used by 1000+ teams. Migration is the highest-risk aspect. Strangler fig: build v2 alongside, migrate table by table.

Phase 1: Shadow Mode (Weeks 1-8) — 3 high-value tables (feed_ranking, web_requests, errors), single DC, dual-ingest into old + new, compare query results within 1%. Teams: Ingestion + Storage + Query Engine.

Phase 2: Dashboard Migration (Weeks 9-16) — Query UI + dashboard builder, migrate top 50 dashboards, dogfood with Observability team, second DC (US-West). Teams: + Dashboard & Viz.

Phase 3: Broad Migration (Weeks 17-28) — Self-serve table creation portal, migrate 100 tables/week via automated tool, third DC (EU), per-table sampling config. Teams: + Schema & Metadata.

Phase 4: Decommission v1 (Months 8-12) — Old Scuba fully decommissioned, anomaly detection online, incident integration.

Rollback Plan

Stage
Rollback Action
Data Impact
Phase 1
Stop dual-write to v2, old Scuba unaffected
None — old Scuba was always source of truth
Phase 2
Redirect dashboard URLs back to old Scuba
None — old Scuba still has all data
Phase 3
Stop table migration, remaining tables stay on old Scuba
Partial — migrated tables on v2, un-migrated on v1, engineers may need to check both
Phase 4
Cannot rollback (v1 decommissioned)
Must fix forward — invest in incident response and automated testing
8

Bottlenecks & evolution

Bottleneck Analysis

Component
Bottleneck
Mitigation
Kafka lag during traffic spikes (300K+ events/sec)
Consumer group falls behind
Auto-scale ingester consumers, 24h Kafka retention buffer, shed low-priority events first
Memory pressure on storage servers
High-volume tables fill memory faster than eviction
Increase sampling rate dynamically, evict oldest partitions (LRU by time bucket), add capacity
Query hot spots (popular tables during SEVs)
Everyone queries same table simultaneously
Replicate hot table partitions on 3+ servers, pre-aggregate, query result cache (30s TTL)
Schema registry as SPOF
All ingesters and query servers depend on schema lookups
Cache schema on every server (60s TTL), fallback to last-known cached version on registry failure
Cross-DC fan-out latency for global queries
Slowest DC dominates response time
Routing heuristics (skip laggy DCs), regional result caching, async pre-fetch for dashboards

Observability (RED Method)

Service
Rate
Errors
Duration
Ingestion Pipeline
Events/sec ingested, batches/sec
Schema validation failures, sampling errors, Kafka consumer errors
Ingest latency (event emission → queryable)
Query Engine
Queries/sec, rows scanned/sec
Query timeouts, partial results, OOM kills
Query latency p50/p99 per table
Columnar Store
Writes/sec (partitions created), scans/sec
Memory allocation failures, partition corruption
Scan latency per partition, compression ratio

Critical Alerts

1. Data freshness > 2 min for any table — ingestion pipeline stalled

2. Query latency p99 > 10s — query engine overloaded or hot partition

3. Memory utilization > 85% on any storage server — risk of OOM, trigger eviction

4. Kafka consumer lag > 5 min — ingesters falling behind, potential data loss window

5. Cross-DC replication lag > 10 min — global queries returning stale data

Distributed tracing: Event emit → Kafka publish (5ms) → broker replicate (10ms) → ingester consume+validate+sample (~15s avg) → columnar write → queryable. Key metric: end-to-end freshness. Target: p50 < 15s, p99 < 30s.

2-3 Year Evolution

Year 1: Foundation

  • All 10K event tables migrated from Scuba v1
  • 3 DCs (US-East, US-West, EU) operational
  • 50,000+ active dashboards
  • Query performance: p99 < 5s, data freshness < 30s
  • Operational maturity: on-call runbook, automated rollback, chaos testing

Year 2: Intelligence

  • Automated anomaly detection on event streams (statistical + ML-based)
  • Correlation engine: link metric anomalies to specific event dimension changes
  • Warm tier: on-disk columnar storage for 7-30 day range (extend retention without memory cost)
  • Adaptive sampling: automatically adjust sample rates based on query patterns and event importance
  • Dashboard templating: standard templates for common service types (web server, ML model, etc.)

Year 3: Platform

  • ML root cause analysis: given an anomaly, automatically identify the dimensional combination explaining it (e.g., "error rate spike is 95% explained by country=BR AND app_version=7.2")
  • Predictive alerting: detect trends before they become incidents
  • Incident management integration: auto-open relevant dashboards in war room during SEVs
  • Natural language query: "show me error rate by country for feed_ranking in the last hour"
  • Self-serve derived metrics: define computed columns and materialized aggregations via config

Cost Optimization Roadmap

Strategy
Timeline
Estimated Savings
Adaptive sampling (auto-tune rates per table)
Month 3
20-30% memory savings (~$600K-900K/mo)
Pre-aggregation for top 1000 dashboards
Month 6
15% query compute savings (~$25K/mo)
Warm tier on-disk columnar (7-30 day data)
Month 9
40% memory savings (~$1.2M/mo)
Reserved capacity pricing (commit 1-year)
Month 6
30% compute savings (~$900K/mo)
Compression improvements (ZSTD, better dictionaries)
Month 12
10-15% memory savings (~$300K-450K/mo)

Summary

Key E6-level insights in this design:

1. In-memory columnar storage is the core architectural choice enabling sub-second aggregation queries over billions of rows — 10-20x compression via dictionary encoding and bit-packing makes this economically viable

2. Priority sampling is the most important cost lever — it determines whether the system costs $4M/month or $25M/month, while preserving 100% fidelity for the events that matter most (errors, tail latency)

3. Scatter-gather query execution with partition pruning ensures query fan-out is proportional to time range, not cluster size — a 1-hour query hits 15 servers, not 1000

4. Schema evolution without downtime via backward-compatible additive changes, with mixed-version partition handling in the query engine

5. Multi-DC with local-first queries optimizes for the common case (debugging your DC’s services during an incident) while supporting global aggregation via cross-DC fan-out

6. Strangler fig migration from existing Scuba v1, with dual-ingest validation and table-by-table migration — the highest-risk aspect of the project and the one that most requires staff-level coordination across 6 teams

Rubric — Senior vs Staff

Dimension
Senior signal
Staff signal
Storage engine
Picks an in-memory columnar store for fast aggregation.
Quantifies row vs columnar vs hybrid on compression, write throughput, and cost; defers the disk-overflow hybrid to a later phase.
Sampling & cost
Samples normal events to fit the memory budget.
Treats priority sampling as the dominant cost lever, guarantees 100% fidelity for errors and tail latency, and corrects counts with error bars at query time.
Query execution
Fans a query out to storage servers and merges the results.
Prunes partitions by time range so fan-out is proportional to the window, with predicate pushdown, vectorized scans, and t-digest percentiles.
Schema evolution
Adds new columns without breaking old events.
Enforces additive-only changes, pushes versions through a config topic, and handles mixed-version partitions inside the query engine.
Multi-DC & rollout
Replicates across datacenters for availability.
Chooses local-first storage with cross-DC fan-out, and sequences a strangler-fig migration with dual-ingest validation and per-phase rollback.
★ MORE WALKTHROUGHS

Want more breakdowns like this?

Join free early access for upcoming RAG, LLM eval, agents, and AI infrastructure walkthroughs.

Join Free Early Access →