Block, drop, or spill — when_full=block paired with a synchronous logger

When the backend stalls, the agent's in-memory queue fills and it must block, drop, or spill to disk — and when_full=block paired with a synchronous logger is the single most common way a logging-pipeline outage takes the application down with it.

Previously

The agent ships at-least-once over a pipe — but ships them WHERE? The backend at the far end can stall, and when it does the agent has exactly three choices, only one of which keeps the application alive.

Scene 03

Block, drop, or spill

  1. Watch
  2. Try it
  3. Predict
  4. Capture
app processrequest handler threadwrite(log_line)synchronous · returns when bufferedthread-poolhealthyBACKENDbackend OKin-mem buffercap00 / 3200events bufferedLINES LOST0shipdisk spillqueue.disk · WAL0 Bon disk · storage.type=filesystemBACKPRESSURE POLICYBLOCKDROPSPILLSPILLDOC ANCHORSFilebeat queue.mem = 3200 events defaultFilebeat queue.disk = 10 GB defaultFluent Bit storage.max_chunks_up = 128Backend healthy. Lines flow through the buffer at steady state — fill climbs, batches ship, fill drains. The …
What to watch for

Backend healthy. Lines arrive at the buffer, fill climbs, a batch ships, fill drains — and the cycle repeats. The app's thread-pool meter is calm; the disk-spill overlay on the right is dim, unused. Get a feel for the steady state before we break the backend.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

App.write_log
synchronous logger calls into the agent's enqueue path
def write_log(line):
# request-handler thread is the caller
if sync_logger:
agent.enqueue(line) # blocks if queue full
return
# async logger: hand to in-process queue, return now
inproc_queue.put_nowait(line)
Agent.enqueue
the when_full policy switch — block, drop, or spill
def enqueue(line):
if len(queue.mem) < queue.mem.capacity:
queue.mem.append(line) # 3200 events default
return
# buffer is full — backend is not draining fast enough
if when_full == 'block':
wait_until_room() # caller's thread parks
elif when_full == 'drop':
metrics.lines_lost += 1 # silently discarded
elif when_full == 'spill':
spill_to_disk(line) # queue.disk / WAL
Agent.spill_to_disk
the disk-backed overflow that keeps the app alive
def spill_to_disk(line):
# queue.disk = 10 GB on Filebeat, storage.type=filesystem
# on Fluent Bit, WAL on Promtail, disk buffer on Vector
disk_buffer.append(line)
def replay_loop(): # runs when backend recovers
while disk_buffer and backend.healthy():
batch = disk_buffer.read_batch()
backend.send(batch) # at-least-once
disk_buffer.advance(batch)

Where this sits in Build a distributed logging stack (ELK / Loki)

Scene 03 of 12. When the backend stalls, the agent must block, drop, or spill to disk — and `when_full=block` plus a synchronous logger is how a logging outage takes the application down with it.

Up next. The agent survived the backend outage by spilling to disk — but every line it spilled was still just a string. Before we ask the backend to index this stuff, we have to decide whether a log line is text or a typed record.

All 12 scenes in Build a distributed logging stack (ELK / Loki) · Every curriculum

Built with Arqly
Every scene in Build a distributed logging stack (ELK / Loki) builds on the one before it.All 12 Build a distributed logging stack (ELK / Loki) scenes