Skip to main content
RunBook Academy

← All break/fix scenarios in Observability

intermediateloki-storage~25 min

Break/Fix: Loki Storage Unavailable

Reported symptoms

  • ●Memory on all three Loki ingesters has been climbing in lockstep since 21:38, with no deploy and no change in ingest volume
  • ●The write-ahead log volume on each ingester is at 61 percent and gaining about 4 GiB an hour, where it normally sits flat at around 8 percent
  • ●Every Loki query is fast and correct - a spot check over the last three hours and another over yesterday both return exactly what the producers say they should
  • ●Grafana panel snapshots have stopped working: the render succeeds and the upload fails
  • ●The nightly platform backup job reported an upload failure at 23:00 and has not been investigated
  • ●Tempo is buffering too, which nobody noticed until somebody thought to look
  • ●The MinIO console reports the cluster ONLINE with 43 percent of capacity used, and an engineer has listed the bucket and downloaded a chunk from a workstation without any difficulty
  • ●A change ticket is open and in progress: routine kernel patching of the storage nodes, approved, applied node by node

Evidence

  • · `sum(rate(loki_ingester_chunks_flushed_total[5m]))` has been zero on all three ingesters since 21:38, while `sum(rate(loki_distributor_bytes_received_total[5m]))` is unchanged
  • · `loki_ingester_memory_chunks` is climbing linearly on all three ingesters from the same minute
  • · `loki_objstore_request_duration_seconds_count` broken out by operation and status code shows GET and LIST returning 200 throughout, and PUT returning 5xx from 21:38 onward
  • · `loki_ingester_wal_bytes` is growing at roughly the ingest rate, and `node_filesystem_avail_bytes` for the WAL volume gives about 95 minutes of headroom at the current rate
  • · `mc admin info` against the storage cluster shows 8 of 16 drives offline, all of them on two of the four nodes
  • · The two nodes with offline drives are the two currently drained by the kernel-patching automation, and both are in rack B
  • · The maintenance automation groups hosts by rack, and two of the four storage nodes are racked together
  • · `mc ls` against the bucket succeeds from a workstation and an object downloads normally, which is what convinced the first responder that storage was healthy
  • · Copying a one-byte test object into the bucket from the same workstation fails
Diagnosis and resolutionclick to reveal

Root cause

The object store is an on-premises MinIO cluster of four nodes with four drives each, arranged as a single erasure set of sixteen. The kernel-patching automation drained two of those nodes at the same time, because its host groups are defined by rack and two of the four storage nodes share rack B. Eight of sixteen drives went offline together. That number sits exactly on the boundary that matters, and the boundary is not one boundary but two: an erasure-coded store needs more drives online to accept a write than it needs to serve a read, because a read only has to gather enough shards to reconstruct an object that already exists, while a write has to place enough shards to satisfy the configured parity for an object that does not. With half the drives gone the cluster is still able to reconstruct everything it holds and is one drive short of being able to store anything new. So it is simultaneously perfectly healthy and completely unavailable, depending on which verb you ask about - and every check anyone ran asked about the read verb. The console reported ONLINE, the capacity graph was unremarkable, a listing worked, a download worked. Loki's response to a failed flush is to keep the chunk in memory, keep the record in the write-ahead log, and keep accepting writes, which is the correct behaviour and is why nothing broke visibly: producers were never rejected, recent queries were served out of ingester memory, and older queries were served from chunks written before 21:38. The platform therefore reported complete health while writing nothing to durable storage for two hours. The other three symptoms are the same fault seen from other angles - Grafana's snapshot upload, the backup job and Tempo's flushes all target the same bucket - and each was raised separately to a different team.

Remediation

Work out the deadline before deciding anything, because in this incident the deadline is the only number that matters. Free space on the write-ahead log volume divided by its growth rate is the time remaining before the ingesters can no longer log records, and that is the moment a recoverable storage outage becomes permanent data loss. Ninety-five minutes is enough to do this properly; twenty minutes would not be. Then restore write quorum, which is cheaper than it sounds: only one of the two drained nodes has to come back to cross the threshold, so the fastest action is almost always to abort the patch on one node and return it, rather than to finish the work on the other. Several tempting actions are wrong and one is dangerous. Do not restart the ingesters: a restart replays the write-ahead log into exactly the same unflushable state, costs the replay time, and drops write capacity while it runs. Do not delete write-ahead log segments to free space - that is the data, and deleting it is the outcome you are trying to prevent. Do not raise the chunk idle period or maximum chunk age to reduce flush pressure, because holding chunks longer increases exactly the memory that is already the problem. If the deadline is genuinely short, the legitimate levers are to grow the write-ahead log volume if the storage layer supports it online, or to shed the lowest-value streams at the agent - a deliberate, recorded, smaller data loss chosen in preference to an undirected one. Hold is a real option and often the right one: if the storage team can return a node in twenty minutes against a ninety-five minute deadline, the correct action is to stop doing things, staff the incident, and set an explicit decision point at which shedding begins, with a named owner and a clock.

Verification

Confirm the write verb specifically, because the read verb never stopped working and will happily tell you everything is fine again. A one-byte object copied into the bucket and then removed is the whole test, and it is the test nobody ran for the first hour. Then confirm Loki is actually flushing rather than merely able to: `rate(loki_ingester_chunks_flushed_total[5m])` must be non-zero on all three ingesters, and `loki_objstore_request_duration_seconds_count` for PUT must be accumulating against a 2xx status code rather than a 5xx one. Watch the backlog drain rather than assuming it does - `loki_ingester_memory_chunks` falls, `loki_ingester_wal_bytes` falls, and the WAL volume returns to its usual few percent. All three should be monotonic once flushing resumes; if memory falls and the WAL does not, something is still failing quietly. Then check for the gap you may not have avoided. Query the incident window and reconcile line counts against what the producers say they sent, because "the write-ahead log held" is a hypothesis until it is measured, and the window where it might not have held is exactly the window nobody will think to check next week. Close out the collateral damage in the same pass: re-run the backup job, confirm a panel snapshot uploads, and confirm Tempo has drained its own buffer. Last, verify the fault cannot recur the same way. The maintenance automation must refuse to drain two nodes of the same erasure set concurrently, and the honest test of that is to ask it to, in a window, and watch it decline.

Prevention

Alert on flushes stopping, not on storage being down. The rate of chunks flushed falling to zero while bytes received stays non-zero is a precise, cheap and unambiguous statement that the durable write path has failed, and it fires within minutes rather than within hours. Almost nobody has this alert, because the intuitive thing to monitor is the storage system, and the storage system was reporting itself healthy for the entire incident. Express write-ahead log headroom as time remaining rather than as percent used. Sixty one percent sounds comfortable and ninety-five minutes does not, and the second number is the one that supports a decision. Make the object-store health check exercise the operation you actually depend on: a synthetic round-trip that writes a small object, reads it back and deletes it tests all three verbs, where a read-only check passes cleanly through a total write outage. This is the same class of blind spot as a monitoring probe that avoids the failing component, and the countermeasure is the same - ask of each check which failure it could not possibly detect. Encode the storage layer's real constraint in the automation that does the draining. The maintenance grouping was by rack because racks are how power and switches fail; the erasure set is how this cluster fails, and nothing in the patching automation knew that erasure sets existed. Any constraint that is only written in a wiki page is not a constraint. Keep the write-ahead log on a volume that can be grown online, because it is the buffer that converts a storage outage into a delay rather than a loss, and its size is the length of outage the platform can survive. And do not let the ruler that would alert on this write its state to the bucket that is broken - an alerting path with a dependency on the failing component is not an alerting path.

Reported symptoms

The page at 23:20 is for ingester memory. All three Loki ingesters have been climbing in lockstep since 21:38. There has been no deploy, and ingest volume has not changed.

The on-call finds four more things wrong, none of which look related:

  • The write-ahead log volumes are filling. All three are at 61 percent and gaining about 4 GiB an hour. They normally sit flat at around 8 percent.
  • Grafana panel snapshots have stopped working. The render succeeds and the upload fails. A ticket was raised at 22:10 and assigned to the Grafana owner.
  • The nightly platform backup failed. It reported an upload error at 23:00. Nobody has looked at it yet.
  • Tempo is buffering too. Nobody noticed until somebody thought to check.

And Loki looks completely healthy. A spot check over the last three hours returns exactly what the producers say they sent. A spot check over yesterday does too. Queries are fast. Nothing has been rejected. No producer has seen an error.

The first responder checks the storage cluster, because ingester memory and storage are related in the runbook. The MinIO console reports ONLINE. Capacity is 43 percent used. They list the bucket from their workstation and download a chunk, which works. They write “storage is fine” in the incident channel and go back to the memory graph.

There is a change ticket open: routine kernel patching of the storage nodes, approved, applied node by node.

Evidence provided

Read-only / Safenothing has been flushed since 21:38
$ promtool query instant http://prometheus:9090 'sum(rate(loki_ingester_chunks_flushed_total[5m]))'
=> 0    (baseline for this hour: about 210/s)

Illustrative output

Read-only / Safeingest is completely normal - everything is arriving and nothing is leaving
$ promtool query instant http://prometheus:9090 'sum(rate(loki_distributor_bytes_received_total[5m]))'
=> 1.09e+07   (30-day baseline: 1.11e+07)

Illustrative output

Read-only / Safereads succeed; the PUT 5xx counter is the only one still moving
$ curl -s http://loki-ingester-0.loki:3100/metrics | grep loki_objstore_request_duration_seconds_count
loki_objstore_request_duration_seconds_count{operation="GET",status_code="200"} 8841203
loki_objstore_request_duration_seconds_count{operation="LIST",status_code="200"} 194402
loki_objstore_request_duration_seconds_count{operation="PUT",status_code="200"} 2210984
loki_objstore_request_duration_seconds_count{operation="PUT",status_code="5xx"} 61147

Illustrative output

Read-only / Safethis number is the deadline
$ promtool query instant http://prometheus:9090 'node_filesystem_avail_bytes{mountpoint="/var/loki/wal"}'
loki-ingester-0  1.44e+11
loki-ingester-1  1.41e+11
loki-ingester-2  1.46e+11

growth: about 4 GiB/h per ingester  ->  roughly 95 minutes of headroom

Illustrative output

Read-only / Safehalf the drives are gone, and the cluster still calls itself online
$ mc admin info storage
4 servers online, 8 drives online, 8 drives offline
minio-a  4 drives online
minio-b  0 drives online   (drained)
minio-c  4 drives online
minio-d  0 drives online   (drained)

Illustrative output

Read-only / Safelisting works, which is what convinced everyone storage was healthy
$ mc ls storage/loki-chunks/ | head -3
[2026-08-18 20:11:04 UTC]  1.4MiB  01JB...c7/
[2026-08-18 20:11:09 UTC]  980KiB  01JB...d1/
[2026-08-18 20:11:12 UTC]  1.2MiB  01JB...d9/

Illustrative output

Read-only / Safeone byte, and the whole incident collapses into one line
$ printf x > /tmp/probe.txt && mc cp /tmp/probe.txt storage/loki-chunks/probe.txt
copy failed

Illustrative output

And from the maintenance automation:

kernel-patch  group: rack-b   hosts: minio-b, minio-d, app-14, app-15, app-16
              started 21:36   mode: drain, patch, return - one host at a time
              note: hosts within a group are drained together

Work the evidence before reading on

Six questions, in the order the evidence answers them.

  1. Bytes received is normal and chunks flushed is zero. What is Loki doing with the difference, and where is it putting it?
  2. Every query returns correct data, including queries over the last hour. If nothing has been written to the object store since 21:38, where is that data being served from?
  3. Reads against the bucket succeed and writes fail. What kind of storage fault distinguishes between the two, and why would a health check never catch it?
  4. Half the drives are offline and the cluster calls itself online. Is that a bug, or is it a precise statement about something?
  5. The snapshot upload, the backup job and the Tempo flush were each raised to a different team. What do those three have in common with each other and with Loki?
  6. The write-ahead log volume has about 95 minutes of headroom. What happens at the end of those 95 minutes, and how does that change what you do in the first five?

Before continuing: the first responder listed the bucket and downloaded an object, and concluded storage was healthy. What question did that test answer, and what question was the incident actually about?

Root cause

Reads and writes do not need the same number of drives

The object store is an on-premises MinIO cluster: four nodes, four drives each, arranged as a single erasure set of sixteen.

An erasure-coded store splits each object into data and parity shards spread across the drives. Reading an object that already exists needs only enough shards to reconstruct it. Writing a new object needs enough drives available to place a complete, correctly parity-protected set of shards - which is more. Those two thresholds are different by design, and the gap between them is exactly where this incident lives.

With eight of sixteen drives offline, the cluster sits in that gap. It can reconstruct everything it already holds and cannot store anything new. It is simultaneously perfectly healthy and completely unavailable, and which one you observe depends entirely on which verb you test.

Every check anyone ran tested the read verb. The console said ONLINE, which is true. Capacity was 43 percent used, which is also true and completely irrelevant - this is not a capacity failure and the free-space graph will never show it. A listing worked. A download worked. The one test that would have ended the investigation in its first minute was a one-byte upload, and it took an hour for anyone to try it.

Two nodes went down together because of how the automation is grouped

The kernel-patching automation drains one host at a time within a group, which is correct and careful. Its groups are defined by rack, which is a reasonable way to group hosts, because racks are how power and switching fail.

The storage cluster’s failure domain is not the rack. It is the erasure set, and two of the four storage nodes happen to be racked together. So an automation working exactly as designed took out half of one erasure set, and nothing anywhere in the pipeline knew that erasure sets existed.

Why nothing broke visibly for two hours

Loki’s response to a failed flush is to keep the chunk in memory, keep the record in the write-ahead log, and keep accepting writes. That is the correct behaviour: a transient storage failure should not become an ingestion failure, and the write-ahead log exists precisely so that the data survives while the durable write path is unavailable.

The consequence is that a total loss of durable storage is invisible from almost every angle:

ObservationWhy it looks fine
Producers see no errorsDistributor and ingester both accept and acknowledge the write
Recent queries are correctQueriers read from ingester memory for data not yet flushed
Older queries are correctThose chunks were written before 21:38 and read fine
Ingest dashboards are greenBytes received never changed
Storage capacity is normalThis is not a capacity failure

What is not fine is that for two hours the platform durably stored nothing, and the only thing standing between a storage outage and permanent data loss is a write-ahead log volume with ninety-five minutes left on it.

The other three tickets are this ticket

The Grafana snapshot upload, the nightly backup and Tempo’s flushes all write to the same bucket. Each failed at 21:38 for the same reason, and each was raised separately, to a different team, as its own small problem.

That fan-out is the most useful pattern in the incident. Several unrelated systems failing in the same minute is rarely a coincidence, and the thing to look for is not what they do but what they depend on.

Resolution

  1. Compute the deadline first, before deciding anything else. Free space on the write-ahead log volume divided by its growth rate is the time until data loss becomes real, and every other decision in this incident depends on whether that number is 95 minutes or 20.
  2. Run the write test. Copy a one-byte object into the bucket. Success and failure mean entirely different investigations, and no amount of listing, downloading or console-reading substitutes for it.
  3. Restore write quorum by the cheapest route. Only one of the two drained nodes has to come back to cross the threshold, so aborting the patch on one node and returning it is almost always faster than completing the work on the other. Confirm with the drive count, not with the cluster status.
  4. Pause the maintenance automation before it moves on to another host. It is working as designed and its design does not know about erasure sets; the next host it drains could be in the other rack.
  5. Do not restart the ingesters. Replay rebuilds the same unflushable state, spends the replay time, and drops write capacity while it runs.
  6. Do not delete write-ahead log segments to reclaim space. That is the data you are trying to save, and deleting it produces the exact outcome the buffer exists to prevent.
  7. Do not raise the chunk idle period or maximum chunk age to reduce flush attempts. Holding chunks longer increases the memory that is already the presenting symptom.
  8. If the deadline is genuinely short, grow the write-ahead log volume online if the storage layer allows it, or shed the lowest-value streams at the agent. Shedding is a deliberate, recorded, bounded data loss chosen over an undirected one, and it needs a name against it in the incident log.
  9. If the storage team can return a node well inside the deadline, hold - and write the hold down. Staffing the incident, stopping other changes and setting an explicit time at which shedding begins is a decision; quietly waiting is not.

Verification

  1. The write verb works, tested directly. A one-byte object copies in and is removed again. The read path never stopped working and will keep reassuring you either way.
  2. Loki is flushing rather than merely able to flush. rate(loki_ingester_chunks_flushed_total[5m]) is non-zero on all three ingesters, and the PUT series of loki_objstore_request_duration_seconds_count is accumulating against a 2xx status code with the 5xx series flat.
  3. The backlog is draining, not just stable. loki_ingester_memory_chunks falls, loki_ingester_wal_bytes falls, and the write-ahead log volume returns to its usual few percent. If memory falls and the log does not, something is still failing quietly.
  4. The gap is measured rather than assumed. Query the incident window and reconcile line counts against what the producers say they sent. That the write-ahead log held is a hypothesis until somebody checks, and this is the only week anyone will remember to.
  5. The drive count, not the cluster status, confirms the storage side. The status field said ONLINE throughout an outage in which nothing could be written, so it is not evidence of anything.
  6. The collateral tickets are closed out in the same pass: the backup job re-runs successfully, a panel snapshot uploads, and Tempo has drained its own buffer.
  7. The maintenance automation now refuses to drain two nodes of the same erasure set together. Test it by asking it to, in a window, and watching it decline. A guardrail that has never refused anything has not been tested.
  8. The new alerts fire on a rehearsal. Take one storage node out deliberately and confirm the flushes-stopped alert and the write-ahead-log-time-remaining alert behave as intended while reads stay green throughout.

Prevention

  • Alert on flushes stopping, not on storage being down. Chunks flushed falling to zero while bytes received stays non-zero is a precise and unambiguous statement that the durable write path has failed, and it fires in minutes. It is also the alert nobody writes, because the intuitive thing to monitor is the storage system - and the storage system spent this entire incident reporting itself healthy.
  • Express write-ahead log headroom as time remaining. Sixty-one percent sounds comfortable and ninety-five minutes does not, and only one of those two numbers supports a decision at 23:20.
  • Make the storage health check exercise the operation you depend on. A synthetic probe that writes a small object, reads it back and deletes it tests all three verbs. A read-only check passes cleanly through a total write outage, which is what happened here.
  • Encode the storage layer’s failure domain in the automation that drains hosts. Grouping by rack is right for power and switching and wrong for an erasure set. A constraint that exists only in a wiki page is not a constraint; it has to live in the thing that would otherwise violate it.
  • Keep the write-ahead log on a volume that can be grown online. Its size is, quite literally, the length of storage outage the platform can survive without losing data. Treat that as a capacity decision rather than a default.
  • Do not let the alerting path depend on the failing component. The Loki ruler persists its state to the same bucket, so an alert about the bucket may be evaluated by something that also cannot write to it. Alerts about the logging platform belong somewhere the logging platform cannot take with it.
  • Read a fan-out of small tickets as one signal. A snapshot upload, a backup job and a trace flush all failing in the same minute is not three problems. Ask what they share before asking what each of them does.