← All runbooks in Observability
Runbook: Recover Loki
1 · Prerequisites
Confirm every item is in place before any state change.
- Loki Architecture Overview
- Loki Configuration
- Loki Storage
- Loki Loss
- Loki Validation
- Loki Backup
- Admin access to the Loki deployment (Kubernetes or systemd) and to the object store bucket
- The Loki configuration in version control, with the commit that is currently deployed identified
- Credentials for the object store that are known to work, tested before the incident rather than during it
- A second operator available. Several steps here are irreversible and are not safe to take alone at 03:00
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · The loss shape is classified as INGESTER, OBJECT STORE or INDEX/COMPACTOR before any action. The three have different first moves, and the first move for one of them destroys the evidence for the other two.
- · The compactor is confirmed STOPPED if any part of the object store is being restored, replaced or repointed. It is the only Loki component that deletes, and it will happily apply retention to a half-restored bucket.
- · The suspect window is written down with a start time in UTC, and it is established whether the platform is losing data NOW or lost data in the past. Those are different urgencies and different procedures.
- · It is established whether the failure is on the write path, the read path, or both. Ingest still working with queries failing rules out the ingester and points at the read path or the index.
- · The object store is confirmed reachable with the credentials Loki is using - not with your own admin credentials, which are almost certainly broader.
- · The current running configuration has been captured from the
/configendpoint before any change, so there is something to diff against afterwards. - · The RPO is stated out loud: how much log data the organisation has already agreed it can lose. Recovery decisions below trade time against data, and that trade needs a number before it needs an operator.
- · A second operator is present and has agreed to the plan. Moving a WAL segment aside and restarting a compactor are both one-way doors.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Classify the loss shape. Read
/readyon each component,/distributor/ringfor ingester states, and the ingester logs. Missing or unreadable WAL is INGESTER. Missing chunks or index objects is OBJECT STORE. Partial results over a specific window with everything else healthy is INDEX/COMPACTOR. Do not proceed until one of the three is named. - 2STOP THE COMPACTOR if the object store is involved in any way. Scale the compactor to zero replicas, or stop its unit. It is the only component in Loki that deletes data, and a compactor running against a partially restored bucket applies retention to gaps and makes the loss permanent. This step precedes every bucket action below and is not optional.
- 3Capture the evidence before restarting anything. Save the ingester logs, the
/configoutput, the ring state and the compactor status. An ingester restarted before its log is read has taken the CRC error, the replay progress and the flush state with it. - 4INGESTER - let the WAL replay finish before concluding anything. A restarting ingester is not ready until replay completes, and replay time scales with the unflushed window: a busy tenant with a long
chunk_idle_periodtakes minutes, a four-hour WAL takes hours./readyreturning not-ready during replay is correct behaviour, not a fault. - 5INGESTER - if replay fails on a corrupt segment, MOVE the segment aside rather than deleting it. The ingester logs a CRC error and refuses to start. Moving the segment lets the ingester start with a truncated WAL; every series in that segment is lost, and the moved file is the only chance of ever accounting for what was in it.
- 6OBJECT STORE - verify the restored bucket contains the window you need BEFORE pointing Loki at it. List objects under the tenant prefix for the affected period. A bucket restored from a stale snapshot is back but incomplete, and the RPO is the snapshot age, not zero.
- 7OBJECT STORE - confirm credentials, region and prefix against what Loki is configured to use. A
301 PermanentRedirecton every request is a wrong region. Access denied on a subset of operations is an IAM policy that covers reads but not the compactor actions, which is a failure that only appears hours later. - 8INDEX/COMPACTOR - clean up a half-built index prefix before restarting the compactor. A compactor killed mid-cycle leaves a partial prefix and refuses to start against it. Move the partial prefix aside; do not delete it until queries over that window are proven correct.
- 9Restore configuration from version control, not from memory. Deploy the known-good commit rather than hand-editing the running config. Then diff
/configagainst that commit and confirm they agree - a hand-edit made during an incident is the drift that causes the next one. - 10Bring components up in dependency order: object store reachable, then ingesters, then queriers, then the compactor LAST. The compactor is the last thing to start and the first thing to stop, for the same reason in both directions.
- 11Confirm the ring converged before trusting anything else.
/distributor/ringmust show every expected ingester ACTIVE. Distributors writing against a stale ring accept pushes and deliver them nowhere, which looks exactly like a healthy recovery for about ten minutes. - 12Prove the platform end to end with a synthetic push and a query for it back. Push one line with a known marker, then query it back. That single round trip proves the distributor, the ingester, the flush path and the read path together; no combination of readiness checks proves the same thing.
- 13Establish the gap boundaries and record them. Which window is missing, which tenants, and whether it was lost or merely delayed. A recovered Loki with an undocumented gap will be queried during the next incident by someone who assumes the absence of lines means the absence of events.
- 14Restore the compactor and watch its first cycle. The first compaction pass after an outage is the most dangerous one, because it is the first time retention is applied to whatever shape the bucket is now in. Watch it complete before closing the incident.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
/readyreturns ready on every component, including the compactor - which is ready only after it acquires its ring lock, so a naive "any 200 is fine" check passes while the compactor is still not running. - ✓
/distributor/ringshows every expected ingester in state ACTIVE. A missing or unhealthy instance means the ring has not converged and writes are being lost silently. - ✓A synthetic line pushed to
/loki/api/v1/pushwith a unique marker is queryable back within seconds. This is the only check that proves the write and read paths together. - ✓A query against a historical window - older than
query_ingester_within, so it must be served from the object store rather than from ingester memory - returns lines. Without this, a broken bucket path stays hidden for roughly thirty minutes. - ✓The compactor status shows a completed cycle with no tables stuck in a compacting state, and its first post-recovery cycle has been watched to completion rather than assumed.
- ✓
loki_canary_last_success(if the canary is deployed) carries a timestamp within the last minute, and the canary write and read counters are both climbing. - ✓The running config from
/configmatches the version-controlled commit that was deployed, field by field for retention and limits. A hand-edit that survived the incident is a defect, not a fix. - ✓The per-tenant overrides are actually loaded, confirmed at
/runtime-configrather than inferred from the file on disk.-verify-configdoes not load the runtime config and will not catch this. - ✓The data gap is written down: start, end, tenants, and lost-versus-delayed. "Loki is back" and "no logs were lost" are two different statements and only one of them has been demonstrated.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Classification and evidence capture are read-only. Everything from the compactor stop onward changes state, and the ordering above exists so the reversible steps come first.
- ↶To undo a compactor stop: start it again. This is safe and is the intended end state - but only after the bucket contents have been verified, never as a reflex to make an alert stop.
- ↶To undo a configuration deploy: redeploy the previous commit and reload, then diff
/configagain. Never leave the running config and the repository disagreeing at the end of an incident. - ↶A WAL segment moved aside cannot be un-moved in any useful sense: the ingester has already started without it and the series it held are gone. Keep the file, record what window it covered, and treat that window as lost.
- ↶A bucket that has had a compactor run against it mid-restore cannot be rolled back. The deletions are real. This is precisely why the compactor stop is step 2 and not step 9.
- ↶A restore that overwrote newer objects with older ones is not reversible either. Restore into a separate prefix or bucket and verify before repointing Loki, whenever the object store offers the option.
- ↶If per-tenant rate limits were raised to absorb a replay flood, lower them again once the backlog drains, and record the change. A limit raised during an incident and never reviewed is how the next capacity surprise is built.
6 · Escalation
When the runbook isn't enough, contact:
- · Escalate before moving any WAL segment aside. It is a deliberate, irreversible data-loss decision, and it needs a second operator and an owner who can accept the gap.
- · Escalate to whoever owns the object store the moment the bucket, its credentials, its region or its lifecycle policy is implicated. Loki configuration changes cannot fix a storage-side problem and will obscure it.
- · Escalate to the platform owner before any restore that could overwrite newer data with older data. The RPO is a business decision, not an on-call one.
- · Escalate to security if the bucket was deleted, emptied or made public by something nobody can account for. An object store holding every retained log line is a compliance event before it is a durability event.
- · Escalate to compliance or legal if the lost window covers a system under a retention obligation. The gap has its own clock and its own reporting duty, independent of the technical recovery.
- · Escalate if recovery time is going to exceed the stated RTO. A partial recovery that restores the read path for recent data while historical restore continues is often the better answer, and that is a decision for the incident owner rather than the operator running the commands.
- · Escalate to the application teams once the gap boundaries are known. They are the people who will otherwise draw conclusions from an empty panel during their own incident next week.
“Recover Loki” is not one procedure. It is three, and they are distinguished by which of Loki’s three durability layers was lost. The layers fail independently, present almost identically to a user, and have first moves that are actively harmful when applied to the wrong one.
distributor <- auth, rate limit, no durable state
|
v
ingester ---> WAL on local disk <- unflushed window, minutes
|
v
chunks + TSDB index ---> object store <- canonical, everything older
^
|
compactor <- the ONLY component
that deletes anything
Read that diagram once more before touching anything. The compactor sits at the bottom with delete authority over the canonical store, and almost every way this runbook can turn a recoverable incident into a permanent one runs through it.
Classify first: which layer was lost
| Shape | What is gone | What it looks like | First move |
|---|---|---|---|
| INGESTER | In-memory streams, and the WAL that would replay them | Recent lines missing; historical queries fine; ingester restarting or refusing to start | Read the ingester log, then let replay finish |
| OBJECT STORE | Chunks and the index | Historical queries empty; the last ~30 minutes still answer from ingester memory | Stop the compactor, then verify the bucket |
| INDEX / COMPACTOR | A coherent index for some window | Partial results over one window; everything else fine | Stop the compactor, then inspect the prefix |
The middle row hides a trap worth naming. When the bucket is gone,
recent queries keep working, because the querier serves the recent
window from ingester memory for query_ingester_within - thirty
minutes by default. For that half hour the platform looks healthy to
anyone who checks “is Loki up” with a five-minute query. The failure
surfaces later, and it surfaces as history, which is exactly when
people trust it most.
When this runbook applies, and when it does not
It applies when Loki cannot return the truth: lines that were ingested are unavailable, or lines that should be ingestible are not being accepted, because a durability layer has been lost or damaged.
It does not apply when:
- Loki is healthy and the logs never arrived. If the distributor never received the bytes, there is nothing to recover here; that is the ingestion path, and it starts on the emitting host.
- The query is wrong. An empty panel against a healthy cluster is a query problem, and running a recovery procedure against it will make things worse for no reason.
- The window is outside retention. The compactor deleted it by policy. That is a retention decision to revisit, not an incident to recover.
- Loki is merely slow. Saturation is a capacity and query-shape problem. Restarting components under load turns a slow platform into an unavailable one.
Blast radius
| Action | Reversible? | What it costs if wrong |
|---|---|---|
Reading /ready, /config, ring and logs | n/a | Nothing |
| Stopping the compactor | Yes - start it again | Retention pauses; index grows. Cheap |
| Restarting an ingester | Partly | Replay time; and any in-memory state not in the WAL |
| Moving a WAL segment aside | No | Every series in that segment, permanently |
| Pointing Loki at a restored bucket | Depends | A stale snapshot silently sets your RPO |
| Running the compactor mid-restore | No | Retention applied to gaps. Permanent deletion |
| Raising limits to absorb a replay flood | Yes | Back-pressure spiral if the flood is larger than the platform |
Two rows are one-way doors. Both are listed in the escalation path because neither should be walked through by one tired operator alone.
Step 1 - Classify, without changing anything
LOKI=http://loki.monitoring.svc:3100
curl -s "$LOKI/ready"
curl -s "$LOKI/distributor/ring" | jq '.instances | map({id: .id, state: .state})'
kubectl logs -n loki -l component=ingester --tail=200 | grep -iE 'wal|replay|crc|error' | tail -20ready
[
{ "id": "loki-ingester-0", "state": "ACTIVE" },
{ "id": "loki-ingester-1", "state": "JOINING" }
]Illustrative output
An ingester stuck in JOINING is usually replaying its WAL, and that
is normal behaviour rather than a fault. /ready is component-aware:
the ingester is not ready until replay finishes, the compactor is not
ready until it acquires its ring lock, and the querier is not ready
until it has loaded index files. A check that accepts any HTTP 200 as
healthy will miss all three.
Step 2 - Stop the compactor, before anything touches the bucket
If the object store is implicated in any way - restored, repointed, partially available, or merely suspected - the compactor stops first.
kubectl scale -n loki deployment/loki-compactor --replicas=0
kubectl get pods -n loki -l component=compactorStopping the compactor pauses retention and lets the index grow. That is the entire cost, and it is trivially reversible. Leaving it running against a bucket that is mid-restore lets it apply retention to a window that only looks old because the objects are not back yet.
Step 3 - INGESTER: replay, and the segment you may have to sacrifice
Let replay finish. Replay time scales with the unflushed window, which
is set by chunk_idle_period and the flush behaviour: a longer flush
interval means a longer WAL, which means a longer recovery. That
relationship is worth remembering at configuration-review time, because
it is where your RTO is actually decided.
If the ingester refuses to start and logs a CRC error against a WAL segment, you are at the one-way door.
# Second operator present, and the window this segment covers recorded first.
WAL_DIR=/loki/wal
SEGMENT=00000042
mv "$WAL_DIR/$SEGMENT" /var/tmp/loki-wal-quarantine-$SEGMENTMoving the segment lets the ingester start with a truncated WAL. Every series in that segment is gone. Keep the file: it is the only artefact that can later answer “what was in the gap”, and deleting it converts an accountable loss into an unexplainable one.
Count series before and after the restart, decide whether the gap fits inside the stated RPO, and escalate if it does not.
Step 4 - OBJECT STORE: verify before you repoint
A restored bucket is not a recovered bucket. Verify that the objects covering the affected window are actually present, under the prefix and in the region Loki is configured to use.
BUCKET=prod-loki-chunks
TENANT=prod
aws s3api list-objects-v2 --bucket "$BUCKET" \
--prefix "index/$TENANT/" --max-items 20 \
--query 'KeyCount' --output textTwo failure shapes are worth recognising by their signature. Every
request returning 301 PermanentRedirect is a region mismatch, not a
permissions problem. Reads succeeding while compactor operations are
denied is an IAM policy that was written against the read path only -
which passes every check you run today and fails at the next retention
cycle.
Restore into a separate prefix or bucket and verify there, whenever the object store offers that option. Restoring in place over newer objects is not reversible.
Step 5 - Bring it back in dependency order
Object store reachable, then ingesters, then queriers, then the compactor last. Deploy configuration from the version-controlled commit rather than hand-editing the running config, then prove the two agree.
loki -config.file=/etc/loki/loki.yaml -verify-config
curl -s "$LOKI/config" | head -40
curl -s "$LOKI/runtime-config" | jq '.overrides | keys'Step 6 - Prove it end to end, then find the gap
Readiness proves processes started. Only a round trip proves the platform works.
MARKER="recovery-$(date -u +%s)"
NOW_NS="$(date +%s)000000000"
curl -sf -X POST "$LOKI/loki/api/v1/push" \
-H 'Content-Type: application/json' \
-d '{"streams":[{"stream":{"job":"smoke"},"values":[["'"$NOW_NS"'","'"$MARKER"'"]]}]}'
sleep 5
export LOKI_ADDR="$LOKI"
logcli query --since=5m --limit=5 --quiet '{job="smoke"}'Then run one query over a window older than query_ingester_within.
Recent queries are served from ingester memory and will succeed even
when the object store path is broken; a historical query is the only
one that exercises the bucket.
Finally, establish the gap. Start, end, tenants, and lost versus merely delayed. A recovered Loki whose gap is undocumented is worse than an obviously broken one, because the next incident will read that empty window as evidence that nothing happened.
Hold, when the queue is draining
If collectors buffered during the outage and are now replaying, the correct action is frequently to watch rather than to act. Lines are arriving late rather than not at all, and a restart during the drain converts late into lost.
Give the hold the same shape as any other action: an owner by name, a checkpoint (ring state and distributor byte rate re-read every five minutes), and an exit condition (the backlog drains, or rejections start climbing, at which point the replay flood is larger than the platform and limits become a real decision).
Common patterns
| What you see | Shape | Where to look |
|---|---|---|
| Recent lines missing, history fine | INGESTER | WAL replay progress in the ingester log |
| Ingester will not start, CRC error | INGESTER | The named WAL segment - this is the one-way door |
| History empty, last 30 min fine | OBJECT STORE | Bucket contents, then query_ingester_within |
| Every bucket request 301 | OBJECT STORE | Region mismatch, not permissions |
| Reads fine, retention never advances | COMPACTOR | IAM policy missing the compactor’s actions |
| Partial results over one window | INDEX | Half-built index prefix from a killed compaction |
| Compactor will not start after a kill | INDEX | Move the partial prefix aside, do not delete |
| Pushes accepted, nothing queryable | RING | /distributor/ring - a stale ring accepts and discards |
| Everything ready, overrides ignored | CONFIG | /runtime-config, not the file on disk |
| Recovery looks fine, gap unknown | PROCESS | Nobody established the boundaries |
Prevention
Three controls decide how this incident goes before it starts.
The first is the WAL on its own volume, backed up on its own schedule. It is the only place the unflushed window exists, and a volume loss that takes both the WAL and the object store turns a bounded gap into an unbounded one.
The second is a compactor that runs as a singleton with leader election. Two compactors racing corrupt the index, which produces the partial-results shape that is the hardest of the three to diagnose.
The third is rehearsing this procedure against a non-production Loki pointed at a separate bucket. Production recovery is a poor place to discover that the credentials in the manifest have never been used for a write. Every step in this runbook is cheap to practise and expensive to learn during an incident.