Storage & Data
Design Distributed Logging Framework
01
What we are building
What we are building
On one server, debugging is ssh and grep. The log file is local, it sits next to the process that wrote it, and the timeline of one request is a few consecutive lines. Move to 1,500 services running as 60,000 short-lived containers and all three of those properties vanish. Containers are rescheduled and their disks disappear with them. One checkout request touches the gateway, the cart, pricing, inventory, payments and an email sender, each on a different node. By the time someone notices a problem, the pod that logged the clue may have been gone for an hour.
We need a central system that collects every log line from every container, stores it durably and cheaply, and lets engineers ask questions such as "show me every ERROR from the payments service in the last 30 minutes that mentions this order ID". It also needs a live view for watching a deploy, and alert rules that fire when error patterns appear. Think of the internal platform behind tools like Grafana Loki, Elastic, Splunk or Datadog Logs.
Ingest never stops and never gets a quiet hour. It must accept bursts several times the average when a bad deploy starts logging stack traces in a loop, without slowing down the services that emit them.
Queries are rare, bursty and urgent. Most look at the last hour of a few services; a few scan weeks of everything for an audit. Both deserve an answer.
At this volume, storage and indexing choices decide whether the platform costs tens of thousands or millions of dollars a year. Cost per gigabyte ingested is a first-class requirement.
Logs only. Metrics and traces are separate systems with different shapes, though we will carry a trace_id field so a log line can link to its trace. Say this early so the interview does not become "design all of observability".
02
Scope and targets
Scope and targets
- A node agent collects stdout, stderr and file logs from every container and attaches labels: cluster, namespace, service, pod, level.
- Structured JSON lines keep their fields; plain text lines are stored as a message with labels.
- Search by time range plus label filters, then by substring, regex or field match, for example
{service="payments", level="error"} |= "ORD-77120". - Live tail for a label selector, with lines appearing a second or two after they are written.
- Alert rules that count matching lines over a window and notify a pager or chat channel.
- Retention set per namespace, from 7 days for debug noise to a year for audit logs, plus delete-by-query for privacy requests.
- Ingest 360,000 lines a second sustained and absorb 3x bursts without dropping data.
- A line is searchable within 10 seconds of being written, and visible in live tail within 2.
- A one-hour search over a few services returns in under 3 seconds; a 30-day scan in under a minute.
- The agent never blocks the application; if the pipeline is down, the agent buffers to local disk.
- Lose no acknowledged data, and stay within a budget of about $0.10 per GB ingested all-in.
How often do people search, and how far back? If 95% of queries touch the last 24 hours, a full-text index over a year of data is wasted money. Are there compliance logs that must be immutable? Do teams log personal data that must be redacted or deletable on request?
03
Estimating the firehose
Estimating the firehose
Start from the fleet: 1,500 services averaging 40 pods each, every pod emitting about 6 lines per second. Average line size including labels is 380 bytes. Queries are counted per engineer: about 4,000 engineers, a handful of searches each on a normal day.
| Quantity | Working | Result |
|---|---|---|
| Pods | 1,500 services x 40 | 60,000 |
| Average line rate | 60,000 x 6 / s | 360,000 / s |
| Burst line rate | 3x during incidents and deploys | about 1.1M / s |
| Raw ingest bandwidth | 360K x 380 B | about 137 MB / s |
| Raw volume per day | 137 MB / s x 86,400 | about 11.8 TB |
| Compressed per day | zstd on sorted chunks, about 9:1 | about 1.3 TB |
| Hot tier, 3 days on SSD | 1.3 TB x 3 x 2 replicas | about 7.8 TB |
| Object storage, 1 year | 1.3 TB x 365, durability handled by the store | about 475 TB |
| Searches per day | 4,000 engineers x about 12, plus dashboards | about 80,000, roughly 1 / s |
| Lines written per line read | 31B written a day, a tiny fraction ever returned | well over 1,000 : 1 |
31 billion lines are written each day and perhaps a few million are ever shown to a person. Any work done per line at write time is multiplied by 31 billion; work done at read time is multiplied by about 80,000. That asymmetry argues for cheap writes and smarter reads.
475 TB in object storage is roughly $11,000 a month at standard list prices, and less with infrequent-access classes. A full inverted index over the same data would typically be one to two times the compressed size again, on fast disks, which is where log platforms usually blow their budget.
04
Interfaces
Interfaces
Three audiences call the platform: agents writing, people searching, and people watching. Each gets an interface shaped for it.
05
Pipeline layout
Pipeline layout
flowchart LR
AG["Node agent with disk buffer"] --> GW["Ingest gateway"]
GW --> K[["Kafka, partitioned by stream"]]
K --> ING["Ingesters build chunks"]
K --> TAIL["Tail fan-out"]
K --> RULE["Rule evaluator"]
ING --> HOT[("Hot chunks on SSD, 3 days")]
ING --> OBJ[("Object storage, chunks and blooms")]
ING --> IDX[("Label index")]
Q["Query frontend"] --> QW["Querier pool"]
QW --> IDX
QW --> HOT
QW --> OBJ
TAIL --> UI["Engineer UI"]
Q --> UIThe pipeline has one rule: every stage can fall behind without hurting the stage before it. The agent buffers to local disk, the gateway acknowledges once Kafka has the batch, and ingesters, tail and alerts read from Kafka at their own pace. A slow indexer delays search; it never makes a checkout service block on a log write.
The agent runs as a daemon on each node, something like Fluent Bit or Vector. It tails container log files, parses JSON, adds Kubernetes labels, batches by size or time, compresses with zstd, and posts to the gateway. It keeps up to 2 GB of unsent data on disk, which covers about an hour of a busy node, and it numbers each batch so the gateway can drop repeats after a retry.
The gateway authenticates the cluster, applies per-tenant rate limits, rejects label sets that would explode cardinality, and writes to Kafka keyed by stream, where a stream is one unique label set. Keying by stream keeps one pod's lines in order on one partition. Kafka holds 24 hours of data, about 12 TB raw, so the downstream stages can be redeployed or rebuilt without data loss.
Ingesters consume partitions and accumulate lines per stream in memory. When a stream's buffer reaches about 1.5 MB compressed or 15 minutes of age, the ingester sorts it by time, compresses it into a chunk, builds a small bloom filter of the tokens in it, writes both to object storage, and records the chunk in the label index. Recent chunks also stay on local SSD for the hot tier. Until a chunk is flushed, queriers ask the ingesters directly for the newest lines, which is how search stays within 10 seconds of real time.
The query path splits a request into time slices, looks up candidate chunks in the label index, drops chunks whose bloom filter rules out the search term, and fans the remaining chunks out across a pool of stateless queriers that decompress and scan them in parallel. Results merge by timestamp and stream back to the UI.
Maps each label set and time bucket to its chunk IDs. Kept small on purpose, since it indexes streams, not words, so it fits in a few hundred GB for a year of data.
Reads Kafka, matches lines against active tail sessions, and pushes them over WebSockets. It is best effort: a slow browser gets lines dropped and a counter, never backpressure into the pipeline.
Streams lines through compiled matchers and keeps sliding-window counters per rule. Evaluating on the stream, not by running searches every minute, keeps alerting off the query path.
Merges small chunks from quiet streams, rewrites chunks for delete requests, enforces retention, and moves older data to cheaper storage classes.
06
Deep dive: storing and finding 31 billion lines a day
Deep dive: storing and finding 31 billion lines a day
Do you build a full-text index over every word at write time, or store compressed chunks with a light index and do the heavy work at read time? The answer decides cost, ingest complexity and query speed.
Why the obvious stores fail
A relational table with one row per line dies on write volume and index maintenance long before a year of data is in it, and a LIKE '%timeout%' query cannot use a B-tree index at all. A general search engine with an inverted index over every token handles queries beautifully, but every line is tokenized, every token appended to posting lists, and segments are merged in the background, all on SSD with replicas. At 360,000 lines a second you end up running a very large cluster mostly to index text that will never be searched.
Chunks, labels and blooms
The chosen layout treats a log stream like a time series of text. Lines are grouped by stream and time into chunks of a few megabytes, compressed together, which works well because neighbouring lines from one pod are very repetitive. The only index is over labels and time: which chunks belong to service=charge-api, level=error between 08:00 and 09:00. That index is tiny because there are perhaps a few million active streams, not trillions of words.
Free-text search then becomes a parallel scan of the chunks that survive the label filter. Two tricks make that scan cheap. First, each chunk carries a bloom filter of its tokens, around 1% of its size, so a search for a rare string like an order ID can skip most chunks without downloading them. Second, the scan itself is embarrassingly parallel: a querier can decompress and grep about 1 GB of raw text per core per second, so 200 querier cores read a 30-minute slice of a large service in well under a second.
flowchart TD
Q["Search: payments errors containing ORD-77120, last 6 h"] --> S["Split into 15-minute slices"]
S --> L["Label index: 9,700 candidate chunks"]
L --> B{"Bloom says token may be present?"}
B -->|no| SK["Skip chunk, about 96% of them"]
B -->|yes| F["Fetch about 400 chunks, from SSD or object store"]
F --> G["Queriers decompress and match in parallel"]
G --> I["Ask ingesters for unflushed lines"]
I --> M["Merge by timestamp, return first 500"]Label cardinality is the thing to police
This design depends on labels describing where a line came from, not what it says. If a team puts a user ID or request ID in a label, every user becomes its own stream, chunks shrink to a handful of lines, the index balloons and compression collapses. The gateway enforces limits, such as at most 15 labels per stream and at most 50,000 active streams per service, and rejects or folds offenders with a clear error. High-cardinality values belong inside the line, where the bloom filter and the scan can find them.
Making common fields fast
Pure scanning is fine for needle searches but slow for questions like "p95 latency by endpoint over a week". For those, a small set of well-known JSON fields such as status, latency_ms and route can be extracted at ingest into a columnar side table in Parquet or ClickHouse. That gives fast aggregations for the 10 fields everyone uses without paying to index the other 500.
Tiers and retention
| Tier | Where | Age | Typical query speed |
|---|---|---|---|
| In memory | Ingester buffers, replicated three ways | Last 15 minutes | Milliseconds |
| Hot | Local NVMe on ingesters and a chunk cache | Up to 3 days | Under 3 s for an hour |
| Standard | Object storage, standard class | 3 to 30 days | Seconds to tens of seconds |
| Archive | Object storage, infrequent or archive class | 30 days to 1 year | Minutes, run as a background job |
07
Design choices weighed
Design choices weighed
Ingest is cheap and storage is mostly object store, so cost scales with bytes, not with words. Needle searches over long ranges are slower than an inverted index, and teams must keep labels low-cardinality.
Best cost per GBFast, flexible search with relevance ranking and rich aggregations. Indexing typically costs several times more compute and storage, and the cluster needs constant care as segments merge. Worth it for a security team that runs heavy ad hoc queries, often on a subset of logs.
For high-value subsetsNew nodes start shipping logs without central configuration, batching happens at the source, and the agent's disk buffer rides out pipeline outages. You must protect the gateway from a flood with quotas.
Simple to run at fleet scaleGives the platform control over load, which is why metrics systems often scrape. For logs it adds polling delay, requires discovering 60,000 short-lived pods, and loses data when a pod dies between polls.
Wrong fit for logsDecouples producers from every consumer, lets search, tail and alerts read the same data independently, and allows replay after a bug. Costs a cluster that holds 24 hours of raw logs.
Absorbs bursts and mistakesFewer moving parts and lower latency. Ingesters then need their own replication and write-ahead log to avoid losing data, and a slow ingester pushes back on agents directly. Loki's own design does roughly this, so it is viable with care.
Viable, more couplingRules see every line within seconds and do not compete with humans for querier capacity. Rules can only use windowed counts and simple matches, not arbitrary joins.
Fast and isolatedAny search can become an alert, which is flexible. But 3,000 rules each searching every minute would become the platform's largest query load and get slower exactly during incidents.
Fails under load08
What goes wrong
What goes wrong
One service starts writing 40 lines per request and its volume jumps 30x in a minute. Without limits, it fills Kafka partitions, slows ingesters for everyone, and the logs needed to debug every other service arrive late.
Per-tenant and per-stream rate limits at the gateway, a 429 that agents respect by buffering locally, and sampling of repeated identical lines with a "repeated N times" marker.An object storage slowdown makes chunk flushes take seconds. Consumer lag climbs to ten minutes. Live tail looks fine because it reads Kafka directly, but search is missing recent lines, which confuses people mid-incident.
Alert on consumer lag in seconds, show a "search is N minutes behind" banner in the UI, autoscale ingesters on lag, and keep 24 hours in Kafka so catching up is just time.A team adds request_id as a label. Active streams jump from 3 million to 400 million in an hour, ingester memory runs out, and the label index grows faster than it can be compacted.
Someone searches 30 days of every namespace for the word "error". The query plans 40 million chunk reads and occupies every querier for minutes during an incident.
The query frontend estimates bytes before running, enforces per-user concurrency and byte limits, queues large scans behind interactive ones, and caches results per time slice so retries are cheap.A node runs out of disk and the agent cannot write its buffer, or the node is terminated before the agent flushes. The last few seconds of logs from that node, often the most interesting ones, are lost.
Reserve buffer space with a quota, flush on SIGTERM during node drain, and report agent drop counters as a metric so silent loss is visible.A debug line prints a full card number or an access token. It is now copied into Kafka, ingesters, the hot tier and a year of archives.
Redaction rules in the agent and the gateway for known patterns, access controls by namespace, and a compactor job that can rewrite chunks to delete matching lines on request.09
How to run the interview
How to run the interview
Begin with grep on one box. Then name what breaks: ephemeral containers, scattered request paths, no alerting. Each part of the pipeline answers one of those.
Say the write-to-read ratio early. When billions of lines are written for every few thousand searches, write-time work is the expensive kind. That one sentence justifies the storage design.
Put a buffer in the middle and explain why. Kafka protects producers from slow consumers and makes replay possible. Mention the local disk buffer in the agent too.
Compare inverted index with chunk scanning. Know the cost of each and when you would pick the expensive one anyway, such as security analytics on a smaller subset.
Bring up cardinality. It is the most common way real log platforms fall over, and interviewers who have run one will light up when you mention it.
Separate tail, search and alerts. Three consumers of the same stream with different guarantees: best-effort, eventually complete, and continuously evaluated.
Talk about cost and retention. Tiered storage, per-namespace retention and sampling of noisy logs show you have seen the invoice.
10
Growing from files to a platform
Growing from files to a platform
Agents send logs to a single server that writes hourly files per service. Search is a parallel grep. It is crude and perfectly fine for a small team.
Adopt a hosted search engine with a short retention, often 7 days. Search becomes fast and flexible, and cost grows linearly with volume.
The search bill becomes the problem. Insert Kafka as a buffer, move bulk storage to compressed chunks with a label index, and keep a full-text index only for the namespaces that need it.
Add bloom filters per chunk, storage tiers, a query frontend with slicing, caching and limits, and streaming alert rules. Gateway limits on cardinality become mandatory.
Keep logs in the region that produced them for cost and data residency, and let a global query frontend fan out to regional queriers. Only results cross regions, never raw chunks.
Watch and read
Further watching and reading
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.