Topic 34

Loki Architecture

Loki

Loki's founding decision fits in one sentence: index the labels, not the text. Elasticsearch builds an inverted index over every word of every line — spectacular for search, expensive to build and store. Loki indexes only each stream's label set, a few kilobytes per stream, and compresses the log content itself into chunks in object storage.

The bet behind that refusal: most operational queries start with "which service, which time window," and a label selector plus a time range answers both without any text index at all. The bet makes Loki dramatically cheaper to run and measurably slower at needle-in-haystack full-text search. Both halves are real, and this topic prices them.

Two log stores, opposite bets on where the work happens
Loki — index the labels
Indexes only each stream's label set (a few KB per stream) and greps compressed chunks in object storage at query time. Cheap to run and store, fast when a label selector plus a time range narrows the search — slow at a needle-in-haystack string hunt.
Elasticsearch — index the text
Builds a full-text inverted index over every word of every line. Any term found in milliseconds across all history and every source — but the index rivals the raw data in size and needs a cluster to operate.

The Founding Trade

In a classic log store, the full-text index is the dominant cost: it often rivals the raw data in size, and it wants fast local disks because every ingested line updates it. Loki refuses to build that index, which removes the cost entirely and moves the work to query time instead. Every design property of Loki, good and bad — the tiny operational footprint, the object-storage economics, the slow rare-string hunt — follows from this one refusal. Hold onto that; it explains everything below.

Streams and Chunks

A stream is one unique combination of label values. Its lines append into chunks — compressed blocks that shrink log text roughly 5–10x and flush to storage at around 1.5 MB each — while the index records only which streams exist and which chunks cover which time ranges. On Harborline, bookings running on two hosts produces exactly two streams.

# one stream = one unique label set
{job="bookings", host="app-01", env="prod"}
{job="bookings", host="app-02", env="prod"}
# per-request IDs ride on the lines themselves — never as labels

With a healthy label set like this, five services on a handful of hosts produce a few dozen streams, and the index stays well under 1% of the stored data. That ratio is the whole economic argument in one number — and it survives only as long as the labels stay few and static.

Object Storage Economics

Chunks and the TSDB index both live in object storage — GCS, S3, or a plain filesystem — at roughly $0.02 per GB-month instead of SSD-backed cluster nodes. Harborline runs Loki as a single binary on obs-01 with filesystem storage, and the architecture is the same one that scales to petabytes when the components run as separate services; retention becomes a lifecycle policy on cheap storage rather than a cluster-resizing project.

Queries as Distributed Grep

Without a text index, a query decompresses and scans chunks — Loki is a distributed, parallelized grep. The label selector and time range narrow the candidate chunk set first; brute force handles the rest. "Errors in bookings in the last hour" scans a few megabytes and returns in well under a second. "This UUID, anywhere, in the last 30 days" scans everything Loki has stored — the exact query Elasticsearch answers in milliseconds is the one Loki grinds through. Which engine is "fast" depends entirely on which query you actually run.

Cardinality Rules Here Too

Every new label value combination mints a new stream, and every stream costs index entries plus its own chunks. Make request_id a label and you get one stream per request: millions of one-line streams, chunks flushed nearly empty, an index bigger than the data it indexes, and ingesters running out of memory. The three costs from Chapter 1 — cardinality, sampling, retention — all apply to logs, and cardinality is the sharpest edge. It is the same disease that killed the Prometheus server in Chapter 3 when someone labeled a metric by user ID, wearing a different uniform.

Structured Metadata

So where do request_id and trace_id go? Loki 3.x's answer is structured metadata: key-value pairs attached to individual lines, filterable at query time, and held out of the stream index entirely. You keep per-request correlation without paying per-request streams. The rule of thumb the rest of the book uses: labels describe where logs come from (job, host, env); structured metadata or the line body carries what they contain.

Loki vs Elasticsearch

Loki indexes only stream labels and greps compressed chunks in object storage at query time: cheap at any volume, operationally small, fast whenever a label selector plus a time range narrows the search. Choose it for service-scoped operational logs — which is most of what an on-call engineer queries.

Elasticsearch builds a full-text inverted index over every line: any term found in milliseconds across all sources and months of history, at the price of index storage rivaling the data and a cluster to operate. Choose it when arbitrary-term search is the workload itself — security, audit, support. Same logs, opposite bets on where the work happens.

Common Mistakes
  • request_id (or user_id, or path) as a Loki label — millions of single-line streams, an index larger than the data, and ingester memory exhaustion. This is the Loki footgun, and it is committed in one line of agent config.
  • Querying {env="prod"} with a line filter over 30 days — a selector matching every stream forces a scan of the entire retention window, and the query times out where a job= selector plus a 2-hour range returns in milliseconds.
  • Letting the agent derive labels from log content (HTTP status, URL path) — each new value mints a stream, and cardinality grows with traffic instead of with topology.
  • Expecting Elasticsearch behavior from Loki — teams benchmark a rare-string search across all history, conclude "Loki is slow," and miss that they bought the cheap half of a trade they never priced.
  • Over-labeling until chunks flush nearly empty — hundreds of streams per service means each chunk fills slowly and ships small, and both storage efficiency and query speed degrade without a single error being raised.
Best Practices
  • Keep labels static and topological — job, host, env, and little else — so a service produces a handful of streams, not a stream per request. If a value can grow without bound, it is not a label.
  • Put per-request identifiers in structured metadata (or leave them in the JSON body) and filter on them at query time — correlation without cardinality.
  • Start every query with the tightest stream selector and the narrowest time range you can defend, because selectors are the only index Loki has.
  • Run Loki against object storage with the TSDB index — the single-store default — so retention is a lifecycle policy on cheap storage, not a cluster-resizing project.
Comparable toolsFull-text Elasticsearch / OpenSearch — the opposite bet, an inverted index over every lineSame bet VictoriaLogs — labels-plus-scan from the VictoriaMetrics familyManaged Grafana Cloud Logs — hosted LokiThird way ClickHouse-based stores — columnar SQL over logs

Knowledge Check

What does Loki index, and what does Elasticsearch index?

  • Loki indexes every word; Elasticsearch indexes only labels
  • Loki indexes stream labels only; Elasticsearch indexes every term of every line
  • Neither indexes anything; both scan raw files at query time
  • They index the same things, but Loki only above a volume threshold configured per tenant

What defines a Loki stream, and what does one more label value create?

  • A stream is one hour of logs; a new label value extends the hour until the chunk flushes
  • A stream is one container; new label values change nothing
  • A stream is one unique label combination; each new value mints another stream
  • A stream is one chunk in object storage

Why is request_id catastrophic as a label but fine as structured metadata?

  • Labels store the value less efficiently than metadata does
  • Structured metadata cannot be queried, so it costs nothing
  • Loki validates labels and rejects high-cardinality ones automatically at ingest time
  • As a label it mints a stream per request; as metadata it stays out of the index

Which query does Loki serve fast, and which does it grind through?

  • Fast: tight selector plus short range. Slow: a rare string across all history
  • Fast: any term across all history. Slow: label-scoped recent queries that miss the index
  • All queries cost about the same, since everything is a scan
  • Fast: ERROR-level queries. Slow: INFO-level queries

You got correct