Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design a Logging Pipeline

Step 1 / 9

Learn system design by building the ingestion pipeline that collects, buffers and delivers logs from thousands of services — the write path specifically, not the query/search side (see Design a…

The numbers to beatboundedbuffer, not unlimiteddisk-spillbefore dropping anythingdrop-oldestlast resort, never block the app

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is a logging pipeline, specifically?

Not the dashboard, not the search box — the unglamorous plumbing before either of those exists: how do log lines from thousands of running service instances get collected, buffered, structured, and delivered somewhere durable, without ever becoming the reason a service itself slows down or falls over.

Applicationemits log lines
New in this step: Application.

We'll build the write path: local buffering that decouples the app from the pipeline's health, backpressure with an explicit policy, batching, structured parsing, delivery guarantees, cost-controlling sampling, and — the sharp edge every real system eventually hits — protecting against a logging storm caused by the very incident you're trying to observe.

What the new pieces do

Applicationclient
A service instance producing log output — potentially thousands of these running concurrently across the fleet.

Step 1 · The baseline

Write straight to the log server

Simplest version: every log line is written synchronously, directly, over the network to a central log server the instant it's produced. What's the danger?

ApplicationLog Store
New in this step: Log Store. · swipe to pan the diagram

A synchronous, direct write to a central log server for every log line. What's the danger?

  1. A single log line is lightweight, but a SYNCHRONOUS network write per line means the application's own request path now depends on the log server's latency and availability — that's not lightweight at all when the log server is slow or down.

  2. Coupling the application's hot path to a synchronous network call for every log line means the log server's health becomes the application's health. An observability tool that can take down the thing it's observing has its priorities backwards.

  3. Disk usage is a real, separate concern (addressed later via sampling), but it's not the IMMEDIATE danger of synchronous writes — the immediate danger is coupling application latency/availability to the logging path.

Synchronous, direct writes couple the application's own request path to the log server's health — if the log server is slow or down, every application doing a synchronous write is now slow or down too. A logging pipeline's first, non-negotiable rule: never let logging become a dependency the application blocks on.

Why this piece earns its place

The failure usually arrives as slowness rather than as an outage, which is what makes it so easy to wave through in review. A log server under load answers in seconds instead of refusing, so every request handler that emits a line sits there holding its thread, its connection and whatever locks it took on the way in. Concurrency drains, the accept queue backs up, and the service is effectively down — with healthy dependencies, passing health checks, and nothing in its own logs to say why, because the logs are exactly what is stuck. The defense is not a shorter timeout; it is removing the wait. And the cheap-looking half-measure — keeping the network write but pushing it onto a background thread inside the process — does not remove it: that in-process queue either grows until the heap is gone or blocks the caller when it fills, and everything still sitting in it dies with the process on precisely the crash you most wanted to read about. Getting the write off the request path is half the fix; getting it out of the process is the other half.

What the new pieces do

Log Storedata
Where parsed, sampled logs land durably — this is what a separate log-search system indexes and queries.

Step 2 · Decouple with a local agent

The app writes locally, an agent forwards

Fix Step 1: the application shouldn't talk to the network log path directly at all.

Applicationemits log linesLog Agentlocal, async
New in this step: Log Agent.

Introduce a local Log Agent — a sidecar or daemon running alongside the application. The app writes to a local, fast destination (stdout, a local file, a local socket) and returns immediately; the agent independently tails that output and forwards it asynchronously. The application's request path is now completely decoupled from the health of anything downstream.

Why this piece earns its place

This step quietly assumes the local write always succeeds and stays cheap, and the thing that breaks that assumption is the disk underneath it. The application writes to a file on the same volume it uses for everything else; if the agent dies, falls behind, or loses its ability to forward, that file grows until the volume is full — and logging has taken the application down again, just by a slower route. So ownership of that file is part of the design: rotation with a hard cap on total size, enforced by whoever writes it rather than by a cron job that may or may not run, and a truncate behaviour the agent tolerates without re-reading from the top. The agent’s read position is the other half. It has to be checkpointed to disk rather than held in memory, or a restart either replays everything already forwarded or skips whatever arrived while it was down — a duplicate flood or a silent gap, chosen by accident rather than by policy.

What the new pieces do

Log Agentgateway
A local sidecar/daemon that tails application output and forwards it asynchronously — the thing that decouples app latency from the health of the logging pipeline.

Step 3 · Backpressure and bounded buffers

What happens when downstream can't keep up?

The agent forwards to a durable queue. That queue — or whatever's past it — gets slow or falls behind. The agent's own local buffer starts filling. What should happen when it fills completely?

Log Agentlocal, asyncDurable Queuebounded, disk-backed
New in this step: Durable Queue.

The agent's local buffer is full because downstream is slow. What's the right policy?

  1. This recreates exactly the Step 1 problem — the application is now blocked on the logging pipeline's health again, just one hop further downstream. Blocking defeats the entire point of the local agent.

  2. An unbounded buffer during a sustained downstream outage will eventually exhaust the host's memory or disk, taking down the AGENT (and potentially the host it runs on) — an unbounded buffer just delays and worsens the failure, not prevents it.

  3. A bounded buffer with a clear, deliberate policy (spill to disk, then drop-oldest under sustained pressure) accepts that during a severe or prolonged downstream outage, SOME log volume may be lost — but the application itself is never blocked, and the host never runs out of resources. This is an explicit, acceptable trade-off, not an accident.

Give the agent a bounded, disk-backed buffer with an explicit overflow policy: spill from memory to local disk under pressure, and if the buffer is still full after that, drop the oldest (or lowest-priority) entries rather than blocking the application or growing without limit. Some log loss during a severe, sustained outage is an acceptable, deliberate trade — an application outage caused by its own logging pipeline is not.

Why this piece earns its place

The knob here is the buffer’s size, and the useful unit is time, not bytes: how long a downstream outage should the agent ride out without losing anything? Multiply the service’s steady log rate by that window and you have your number. Sizing by a round figure of megabytes instead means the quietest service survives for days and the noisiest for well under a minute on the same setting, which is how one number ends up wrong everywhere. Too small and an ordinary queue deploy costs you logs; too large and the spill file is competing for the same disk and the same IO the application needs. The direction of the drop deserves its own argument, because oldest-first sounds like the careless choice and is in fact the right one — the buffer only fills when something is going wrong, and the lines describing what is going wrong now are the ones arriving now. Dropping newest to preserve the backlog would keep the calmest minutes and discard the only ones anyone is going to read.

  • boundedbuffer, not unlimited
  • disk-spillbefore dropping anything
  • drop-oldestlast resort, never block the app

What the new pieces do

Durable Queueevents
A durable buffer (think Kafka) absorbing bursts and outages downstream — bounded, with an explicit policy for what happens when it fills.

Step 4 · Batch for throughput

Don't send one network call per line

Forwarding every single log line as its own network write to the durable queue is enormously wasteful of connection overhead at real log volume. What's the fix?

ApplicationLog AgentDurable QueueLog Store
The system as it stands at this step. · swipe to pan the diagram

Batch log lines — accumulate either N lines or T milliseconds of output, whichever comes first, then send one batched write. This amortizes network and write overhead across many lines, at the cost of a small, bounded delay (milliseconds to a couple seconds) before a given log line is actually durable downstream — an easy trade for the throughput gain at real scale.

Why this piece earns its place

Both halves of that trigger earn their place, and dropping either one fails in the opposite direction. Flushing on count alone strands a quiet service’s lines in the buffer indefinitely — nothing is lost, it simply never arrives, which during a low-traffic incident amounts to the same thing. Flushing on time alone throws away the efficiency on exactly the busy services that needed it. So set the timer by what someone tailing logs mid-incident will tolerate before they stop trusting the screen, set the count by the payload size the queue is happiest accepting, and let whichever fires first win. Going long on either costs more than latency: the batch is the unit of loss if the host dies before a flush, and it is also the unit of retry, so a larger batch means resending a lot of good lines to recover one bad send. That is also why the delivery key that comes next is per batch — the batch, not the line, is the thing this pipeline actually moves.

Step 5 · Where does parsing happen?

Raw text vs. structured fields

A raw log line is just a string. Downstream search, alerting, and dashboards need structured fields — timestamp, level, service name, trace id. Should parsing happen at the agent (distributed, at the edge) or centrally, after the queue?

Durable Queuebounded, disk-backedStructured Parsertext → fieldsLog Storefeeds search/alerts
New in this step: Structured Parser.

Turning raw text into structured fields — parse at the edge (agent) or centrally, after the queue?

  1. Edge parsing distributes CPU cost nicely, but with potentially thousands of independent agent instances each running parsing logic, small drift or bugs in parsing rules across different service versions/deployments become genuinely hard to keep consistent and hard to fix centrally.

  2. Centralizing parsing means one codebase, one place to add a new log format or fix a parsing bug, and one thing to monitor for parsing errors — the trade-off is that this component now has to scale with the FULL aggregate log volume, which is a real, but very solvable (horizontally scalable) capacity planning problem.

  3. Unparsed raw text makes downstream search and alerting radically slower and less capable (full-text scans instead of indexed field queries) — structuring at ingest time is what makes the rest of the observability stack fast.

Centralize structured parsing in a dedicated Structured Parser service reading from the durable queue — one place to define and evolve parsing rules across every log format, at the cost of a component that must scale horizontally with total log volume (a well-understood, solvable scaling problem, unlike inconsistency across thousands of independent edge parsers).

Why this piece earns its place

Consistency cuts both ways: the cost of having one place to fix a rule is that it is also one place to break every service at once. A bad rule on an edge agent corrupts the logs of whatever it runs beside; the same rule shipped to a central parser corrupts the whole fleet’s logs in the same minute, and the incident it obscures will be the one you are trying to read through. That makes two things non-negotiable here. The raw line travels with the structured fields, so a bad rule is recoverable by reparsing rather than a permanent loss of the original text. And a line that fails to parse is never dropped — it goes to the dead-letter path, so an unanticipated format shows up as a queue somebody can look at instead of as a service that appears to have gone quiet. Beyond that, rules get versioned and canaried against a slice of the live stream, because the cheapest place to find out a rule is wrong is on traffic nothing is depending on yet.

What the new pieces do

Structured Parserservice
Turns raw log lines into structured fields (level, timestamp, service, trace id) so downstream search and alerting are fast.

Step 6 · Delivery guarantees

At-least-once, plus dedup

A batch fails to deliver — a network blip mid-send. The agent retries. What if the batch had actually landed the first time, and the retry creates a duplicate?

Structured Parsertext → fieldsLog Storefeeds search/alertsDead-Letter / Retryfailed deliveries
New in this step: Dead-Letter / Retry.

A delivery might succeed even though the sender thinks it failed and retries. How do you avoid double-ingesting?

  1. Silently losing logs on any transient network hiccup is a much worse trade than occasionally handling a duplicate — logs are often needed precisely to debug the incident that caused the network hiccup in the first place.

  2. At-least-once delivery (retry until acknowledged) guarantees nothing is silently dropped due to a transient failure, and pairing it with an idempotency key (a deterministic id per batch) lets the downstream store recognize "I've already ingested this exact batch" and discard the duplicate rather than double-counting it.

  3. True exactly-once delivery across independent network hops is a notoriously hard distributed-systems problem with real coordination overhead — at-least-once plus idempotent dedup achieves the same practical outcome (no duplicates ingested) far more simply.

Use at-least-once delivery — retry on any uncertain outcome — paired with a deterministic idempotency key per batch, so the downstream path (via the Dead-Letter/Retry store for anything that needed a retry) can recognize and discard a true duplicate rather than double-ingesting it. This achieves effectively-once ingestion without needing distributed transactions.

Why this piece earns its place

Both halves of this have a way of failing that nothing reports. The key has to be sealed with the batch and persisted alongside it, not computed at send time from whatever the batch happens to contain — an agent that restarts mid-retry and re-forms the batch from its buffer will sweep in a few lines that arrived meanwhile, hash to a different key, and the store dedups nothing. The retry lands as a perfectly clean duplicate wearing a fresh id. So a batch gets its identity once, when it is closed, and carries it through every attempt. The other half is the window: no store remembers every key it has ever seen, it remembers recent ones, and a retry that finally succeeds after a long outage can fall outside that memory and be ingested twice. The dedup retention therefore has to outlast the longest backoff the agent is permitted to take — which means the retry policy and the store’s retention are one decision made in two places, and they drift the day somebody tunes the backoff on its own.

What the new pieces do

Dead-Letter / Retrydata
Holds records that failed to parse or deliver on the first attempt, retried on a backoff instead of being silently dropped.

Step 7 · Sampling at high volume

You don't need 100% of everything

A fleet of thousands of instances emitting verbose debug-level logs can produce a genuinely enormous, expensive volume — most of it routine and never looked at. Should the pipeline ingest all of it?

Durable Queuebounded, disk-backedStructured Parsertext → fieldsSamplercost controlLog Storefeeds search/alertsDead-Letter / Retryfailed deliveries
New in this step: Sampler.

Ingesting 100% of debug/info-level logs at fleet scale is expensive and most is never read. What's the fix?

  1. Errors and warnings are exactly the events you can't afford to miss, so they're never sampled away. Routine, high-volume, low-signal logs are the right target for sampling — even keeping 1-5% of them still gives you a statistically useful picture of normal behavior at a fraction of the ingestion cost.

  2. Disabling routine logging entirely removes a genuinely useful signal (the normal-behavior baseline you compare against when something goes wrong) — sampling keeps a representative slice instead of throwing the whole category away.

  3. Compression helps but doesn't change the fundamental volume being processed, indexed, and stored — at extreme scale, the ingestion and indexing cost of the raw event COUNT, not just its compressed size, is often the dominant expense.

Introduce a Sampler: always keep 100% of high-signal logs (errors, warnings, anything tagged critical), but sample a smaller, representative fraction of high-volume, low-signal logs (routine debug/info). This preserves the ability to debug real incidents fully while controlling the cost of ingesting the much larger volume of routine noise.

Why this piece earns its place

Sample the wrong unit and you hold the volume down while destroying the thing logs are for. A per-line coin flip leaves every request holding a scattered handful of its own lines, so there is plenty of data and not one complete story to follow. The decision has to be made per request — hash the trace id and let that choose — so a request that is kept keeps all of its lines and a request that is dropped contributes none. It is also worth being honest about where the saving lands. The Sampler sits after the queue, so the agent, the network and the queue still carry the full flood; what gets cheaper is the store and everything indexing behind it. That is a real and usually dominant cost, but it means sampling is not a capacity control for the pipeline itself, and reaching for a lower sampling rate because the queue is struggling is treating the wrong component. The pipeline needs its own limit, further upstream.

What the new pieces do

Samplerservice
Keeps all high-signal logs (errors) and samples the flood of routine, low-signal ones — because ingesting 100% of everything is often prohibitively expensive at real scale.

Step 8 · The sharp edge

The logging storm

An incident hits: a retry loop somewhere starts logging an error on every failed attempt, at high frequency, across many instances simultaneously. Log volume spikes 100x in seconds — right when you most need the pipeline to be reliable and fast.

ApplicationLog AgentDurable QueueStructured ParserSamplerLog StoreDead-Letter / Retry
The system as it stands at this step. · swipe to pan the diagram

Add per-service rate limiting on the logging path itself, distinct from the sampling policy: if a single service's log volume spikes far beyond its normal baseline, cap it (drop excess volume beyond the cap, optionally keeping a representative slice) rather than letting it saturate the agent's buffer, the queue, and potentially the app itself via the Step 3 backpressure policy. This is the final application of the Step 1 principle: the observability system must never amplify the very incident it exists to help you debug.

Why this piece earns its place

A baseline-relative limit has a cold start, and the day it bites is a deploy. A service that has just shipped has no established normal, so it either runs unthrottled — precisely the wrong exemption for code that has never run in production before — or it inherits a default that throttles a legitimately chatty new service on its first day. The honest answer is to hold new services on a conservative default and let the baseline earn them a higher one over a settling period. Scope is the other trap: the limiter lives at the agent, which sees one instance, while the storms worth fearing are fleetwide, and a crash loop can trip no single instance’s cap at all while the fleet multiplies it by however many instances are running. A per-instance cap needs a fleetwide view behind it. And whatever the limiter drops has to be counted and reported as its own signal, because on a log-volume graph a limiter working perfectly looks identical to a service that has gone silent — and those two readings send on-call in opposite directions.

You did it

You just designed a logging pipeline.

ApplicationLog AgentDurable QueueStructured ParserSamplerLog StoreDead-Letter / Retry
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Synchronous, direct writes couple application health to log-server health — the pipeline must never become a dependency the app blocks on.
  • A local agent decouples the app from the network path entirely, buffering and forwarding asynchronously.
  • A bounded, disk-backed buffer with an explicit drop-oldest overflow policy prevents the agent from either blocking the app or exhausting host resources.
  • Batching amortizes write overhead across many log lines for a small, bounded delivery-latency cost.
  • Structured parsing is centralized for consistency, at the cost of a component that must scale with total log volume.
  • At-least-once delivery plus an idempotency key achieves effectively-once ingestion without distributed-transaction overhead.
  • Sampling keeps 100% of high-signal logs (errors) and only a representative fraction of high-volume, low-signal ones.
  • Per-service rate limiting on the logging path itself prevents a logging storm from turning an incident into an outage caused by its own observability tooling.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. How is this different from "Design a Log Search System"?

    This design covers the WRITE path — how logs get collected, buffered, parsed, delivered, and cost-controlled before they land durably anywhere. A log search system covers the READ path on top of that: indexing (often an inverted index for full-text search), query planning, and fast retrieval over logs that are ALREADY durably stored. They're genuinely different engineering problems — one is about reliable, high-throughput ingestion under backpressure; the other is about fast querying over a large indexed corpus — and a real observability platform needs both, built and reasoned about somewhat independently.

  2. Why not just always keep the buffer unbounded and accept the memory cost, given how important logs can be for debugging?

    The problem isn't that logs are unimportant — it's that an unbounded buffer during a SUSTAINED downstream outage (not a brief blip) will eventually consume all available memory or disk on the host running the agent, which can degrade or crash the AGENT itself, or even the application host it shares resources with. At that point you've lost far more than the logs you were trying to preserve — you've potentially taken down the application too. A bounded buffer with disk-spill accepts a graceful, deliberate, limited amount of log loss specifically to guarantee the pipeline itself stays healthy under sustained pressure, which is the trade Step 1's core principle demands.

  3. Could sampling cause you to miss the specific instance of an error that would have explained an incident?

    This is exactly why error/warning-level logs are deliberately EXEMPT from sampling — the sampler in Step 7 keeps 100% of high-signal events specifically because those are the ones you can't afford to lose even one of. Sampling only applies to routine, high-volume, low-signal logs (debug/info), where losing 95% of them still leaves a statistically representative picture of normal behavior — the exact log line that would explain an incident is, by definition, usually an error or warning, which was never subject to sampling in the first place.

  4. What's the actual mechanism that distinguishes a logging storm from a normal, legitimate high-traffic period?

    The rate limit in Step 8 is typically defined RELATIVE to each service's own established baseline volume, not an absolute global number — a service that normally logs 100 lines/second spiking to 10,000/second is almost certainly a storm (a retry loop, a crash loop) regardless of whether OVERALL fleet traffic is high or low that day, whereas a service whose normal baseline genuinely is 10,000 lines/second isn't flagged just for being naturally high-volume. This per-service, baseline-relative approach distinguishes "this specific service just started behaving abnormally" from "the whole system is just busy today," which a single global threshold couldn't do correctly.

  5. Why centralize parsing but decentralize buffering/batching — isn't that an inconsistent design philosophy?

    It's not inconsistent — it's the same underlying principle (put each concern where its actual requirements are best served) applied twice, with different conclusions because the concerns have different requirements. Buffering/batching needs to happen AS CLOSE TO THE APPLICATION as possible specifically to achieve latency-decoupling (Step 1's core requirement) — that only works if it's local and distributed. Parsing has no such latency-coupling requirement (the app already returned immediately after Step 2); what parsing actually needs is CONSISTENCY of rules across every log format and service, which is best achieved by having one definition in one place. Different requirements, so different placement — not an inconsistency.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Synchronous, direct writes to a central log server are dangerous because…

    An observability tool that can slow down or block the application it's observing has its priorities backwards.

  2. When the agent's local buffer is completely full, the right policy is generally to…

    Bounded loss during a severe outage is an acceptable trade; an app outage caused by its own logging pipeline is not.

  3. Structured parsing is centralized (rather than done independently per agent) mainly because…

    Parsing rules benefit from consistency; the trade-off is a component that must scale with total log volume.

  4. At-least-once delivery with an idempotency key achieves…

    Retry-until-acknowledged plus dedup at the consumer is the pragmatic, low-coordination-cost path to effectively-once processing.

  5. A logging storm is specifically dangerous because…

    Per-service rate limiting on the logging path prevents the observability system from amplifying the incident it exists to help debug.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Decouple: the app writes locally and returns; a local agent forwards asynchronously — never a blocking dependency.
  • Backpressure: a bounded, disk-backed buffer with an explicit overflow policy (spill, then drop-oldest).
  • Batch: accumulate N lines or T ms, then one batched write to amortize overhead.
  • Deliver: at-least-once with an idempotency key so the store dedups true duplicates.
  • Control cost: sample low-signal logs (keep 100% of errors) and rate-limit a logging storm.

The qualities that shape everything

Each one names the mechanism that buys it.

Logging never becomes a dependency the app blocks on
A local Log Agent lets the app write to a fast local destination and return, forwarding asynchronously so the request path is decoupled from anything downstream.
A slow downstream never blocks the app or exhausts the host
A bounded, disk-backed buffer spills to disk under pressure and drops the oldest entries rather than blocking the app or growing without limit.
Efficient delivery at real log volume
Batch N lines or T milliseconds into one write, amortizing network/write overhead for a small bounded delay.
Parsing rules stay consistent across the fleet
Centralize structured parsing in one service reading from the queue — one place to define and fix rules — accepting a component that scales with total volume.
No silent loss and no double-ingest
At-least-once delivery plus a deterministic idempotency key per batch, so the store discards true duplicates — effectively-once without distributed transactions.
A logging storm can’t amplify the incident
Per-service, baseline-relative rate limiting on the logging path drops excess volume before backpressure ever reaches the app.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A local agent forwarding async over synchronous writes to the log server

A synchronous network write per line couples the app’s request path to the log server’s health — an observability tool that can take down what it observes; a local hop decouples them entirely.

A bounded buffer, drop-oldest over blocking the app when the buffer fills

Blocking recreates the very coupling the local agent removed — the app is back to waiting on the pipeline; a bounded buffer accepts some log loss under a sustained outage but never blocks the app.

Centralized parsing over parsing at every edge agent

Thousands of independent agent parsers drift and are hard to fix centrally; one parsing service is a single place to define and evolve rules, at the cost of scaling with total volume.

At-least-once + idempotency key over a distributed transaction for exactly-once

True exactly-once across network hops is a hard, coordination-heavy problem; retry-until-acked plus a dedup key achieves effectively-once ingestion far more simply.

Sample low-signal, keep all errors over ingesting 100% of everything

Compression doesn’t change the raw event count that dominates cost at scale; keeping every error but sampling routine debug/info preserves debuggability at a fraction of the cost.

The answer, out loud

What a strong answer to “Design a Logging Pipeline” sounds like, first question to last trade-off. It is about 6 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    Scope the write path

    Let me pin down which half of this we’re building, because logging is really two systems. There’s the write path — getting lines off thousands of running instances and somewhere durable — and the read path, indexing and querying what already landed. I’m designing the write path. The functional requirements are collect, buffer, structure, deliver, and control the cost. But the requirement I’d write above all of those is a negative one: the logging pipeline must never become a dependency the application blocks on. Almost everything after this is that one rule applied again.

  2. 4–9 min

    Start from the naive version

    The first thing anyone builds is a synchronous network write to a central log server for every line, and I want to start there because the danger is instructive. That call puts the log server’s latency and availability directly into the application’s own request path. If the log server is slow, every service writing to it is slow. If it’s down, they’re down. An observability tool that can cause the outage it exists to explain has its priorities backwards, so that coupling is the first thing I design out.

    Built in step 1: Write straight to the log server
  3. 9–15 min

    Decouple with a local agent

    So the application doesn’t talk to the network log path at all. It writes somewhere local and fast — stdout, a local file, a local socket — and returns immediately. Running alongside it is a log agent, a sidecar or daemon, which tails that output and forwards it asynchronously to a durable queue. A local write is far faster than a network call and fails independently of anything remote, so the request path is now decoupled from the health of everything downstream. That’s the structural move the rest of the design hangs off.

    Built in step 2: The app writes locally, an agent forwards
  4. 15–22 min

    What happens when the buffer fills

    Now the interesting question. Downstream slows down, the agent’s buffer fills — what should happen? Blocking the application is the worst option; that’s the first problem again, one hop later. Growing without limit only delays the failure and takes the host with it when it comes. So it’s a bounded buffer with a policy I’d say out loud: spill from memory to local disk first, and if that fills too, drop the oldest entries. I’m accepting some log loss during a long outage to guarantee the application never stops. That’s a deliberate trade, not an accident.

    Built in step 3: What happens when downstream can't keep up?
  5. 22–27 min

    Batch the forwarding

    On the forwarding side, one network write per log line spends nearly all of its effort on overhead. So the agent batches — it accumulates either a number of lines or a few milliseconds of output, whichever comes first, and sends one write. A given line becomes durable slightly later than it otherwise would have, bounded and small, and in exchange the pipeline moves far more volume on the same hardware. At real log rates that’s an easy trade to defend.

    Built in step 4: Don't send one network call per line
  6. 27–33 min

    Parse centrally, not at the edge

    Raw lines are just strings, and search and alerting need fields — timestamp, level, service, trace id. The question is where that happens. I’d put it in a parsing service reading off the queue rather than in every agent. With thousands of independent agents, parsing rules drift across service versions and a bad one is hard to fix everywhere at once. Centralizing gives me one place to define, version and correct them. The cost is a component that has to scale with the full aggregate log volume, which is a horizontal scaling problem I know how to solve.

    Built in step 5: Raw text vs. structured fields
  7. 33–38 min

    Delivery guarantees

    For delivery I’d take at-least-once plus dedup rather than chase exactly-once. A send can fail after the data actually landed, so retrying until acknowledged is the only way not to lose batches on a transient blip — and logs are usually wanted precisely to debug that blip. The duplicate retrying creates is handled by attaching a deterministic idempotency key to each batch, so the store can recognise one it has already ingested and discard it. That gets me effectively-once without a distributed transaction across every hop.

    Built in step 6: At-least-once, plus dedup
  8. 38–42 min

    Sample what nobody reads

    Cost is the last functional concern. A fleet emitting verbose debug logs produces an enormous volume that is mostly routine and mostly never read, and compression doesn’t rescue it because the expense tracks the event count, not just the bytes. So I’d sample: keep everything high-signal — errors, warnings, anything tagged critical — and keep only a representative fraction of routine debug and info. The rare line that ends up explaining an incident is almost always one of the ones I never sampled.

    Built in step 7: You don't need 100% of everything
  9. 42–45 min

    Close on the trade-off

    I’d close on the sharp edge. When an incident hits — a retry loop, a crash loop — log volume spikes by orders of magnitude in seconds, which is the moment the pipeline is least able to absorb it and most needed. So there’s a per-service rate limit on the logging path itself, capping a service that has jumped far past its own normal before the storm ever reaches the drop-or-block decision. The trade running through the whole design is that I accept bounded log loss to protect the application. With more time I’d work through what survives losing the queue entirely, and how the agent proves afterwards what it dropped.

    Built in step 8: The logging storm

What this teaches

Learn system design by building the ingestion pipeline that collects, buffers and delivers logs from thousands of services — the write path specifically, not the query/search side (see Design a Log Search System for indexing and querying already-ingested logs). An interactive guide covering why synchronous writes to a central log server are dangerous, local agent buffering, backpressure and bounded buffers, batching for throughput, where structured parsing should happen, at-least-once delivery with dedup, sampling at high volume, and why a logging storm can take down the very system it was meant to observe.

Key takeaways

  • Synchronous, direct writes couple application health to log-server health — the pipeline must never become a dependency the app blocks on.
  • A local agent decouples the app from the network path entirely, buffering and forwarding asynchronously.
  • A bounded, disk-backed buffer with an explicit drop-oldest overflow policy prevents the agent from either blocking the app or exhausting host resources.
  • Batching amortizes write overhead across many log lines for a small, bounded delivery-latency cost.
  • Structured parsing is centralized for consistency, at the cost of a component that must scale with total log volume.
  • At-least-once delivery plus an idempotency key achieves effectively-once ingestion without distributed-transaction overhead.
  • Sampling keeps 100% of high-signal logs (errors) and only a representative fraction of high-volume, low-signal ones.
  • Per-service rate limiting on the logging path itself prevents a logging storm from turning an incident into an outage caused by its own observability tooling.

Concepts covered

  • What is a logging pipeline, specifically?
  • Write straight to the log server
  • The app writes locally, an agent forwards
  • What happens when downstream can't keep up?
  • Don't send one network call per line
  • Raw text vs. structured fields
  • At-least-once, plus dedup
  • You don't need 100% of everything
  • The logging storm
built so a logging incident never becomes the incident — make the calls, flood it with a log storm, run the gauntlet.
Finished this one? 0 / 65 System Designs done
NextDesign a Key-Value Store Closest system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs