Segments: how writes really work

The Elasticsearch essays · December 2025

Here is a puzzle. Elasticsearch's inverted index is heavily compressed and interleaved on disk; inserting one document into the middle of it would mean rewriting enormous files. Yet the machine accepts hundreds of thousands of writes per second. This essay is about the trick that makes it possible, and the live simulation below lets you run it yourself.

The answer, inherited from the Lucene library at its core, is one of the great tricks of systems engineering: never modify files, only create new ones. Incoming documents accumulate in a memory buffer. On every refresh, the buffer is written out as a small, complete, immutable mini-index called a segment. A search simply asks every segment and combines answers. Deletion doesn't touch the segment either; the document is marked dead in a side file and filtered from results. And a background process constantly merges small segments into bigger ones. Below, the whole machinery is running live; you control its dials:

6/s
1s
6
1/s
Documents stream into the buffer; each refresh seals it into an immutable segment; red slivers are deleted docs, merely tombstoned; when enough segments pile up, the smallest merge into one and their tombstones finally vanish.
Experiments to try: slow the refresh to 5s and watch segments get big and searchability lag; set merge at 12 and watch the segment count (and the cost of every search) climb; crank deletes and see how much dead weight merging reclaims.

Every oddity of Elasticsearch falls out of this design. Why "near real-time" rather than instant? A document is searchable only after the next refresh. Why doesn't a crash lose the buffer? Every write is also appended to a plain journal, the translog, replayed on restart. Why does the machine spend idle time merging segments? Because searching a thousand tiny indexes is slower than ten large ones, and merging is when deleted documents are finally, physically, gone.

A detail the simulation glosses: merges aren't random. The policy is tiered: it looks for segments of similar size to combine, so bytes get rewritten roughly logarithmically many times over their life rather than again and again. And once an index stops receiving writes forever (yesterday's log index, say), a force-merge down to a single segment is the final kindness: one segment, maximum compression, minimum per-search overhead.

The visibility dial

The simulation above refreshed once per second, the default, but that number is a choice, and it prices a real trade. (Elasticsearch already bends it: unless you set the interval yourself, a shard that has not been searched for 30 seconds stops refreshing until the next search arrives, so write-only indices skip the cost.) Every refresh cuts a segment, and small segments cost merges later; refresh rarely and indexing gets cheaper while new documents stay invisible longer:

A busy index taking 50 GB a day. At one second it cuts 86,400 tiny segments, and merging them up to full size rewrites every byte about four times. At thirty seconds: 30× fewer segments, each 30× bigger, about one and a half rewrites saved per byte, and a fresh log line takes up to half a minute to appear in search. For a human tailing logs that is an eternity; for a nightly batch load, even refresh-on-demand is fine. There is no right answer, which is exactly why it is a setting.

Holding back time

Immutability hands out one more gift: a consistent view of an index is just a list of segments. Hold that list, and you hold a frozen moment, no matter what happens next. That is a point-in-time, and it is how a paginating client avoids seeing the data shift under its feet:

–
segments on disk
–
retained only for the PIT
The top row is the index as new searches see it: merges have replaced the old segments. The bottom row is the frozen list the client's PIT still references. The outlined segments have been merged away from the live view, but the files stay on disk because one reader still needs them; release the PIT and they vanish on the next cleanup. This is also why a forgotten long-lived PIT quietly inflates disk usage: it is a promise the machine keeps.

The crash question

One worry should be nagging you: documents sit in a memory buffer until the next refresh, and even a fresh segment isn't guaranteed on disk until a periodic flush commits it. What if the machine dies in between? The answer is the translog: before anything else happens, every write is appended to a plain sequential journal and fsynced. Replaying a journal is trivial; that's the point. Pull the plug yourself, at any moment:

Six documents arrive over time; the machine refreshes twice and flushes once. Drag the crash moment anywhere: documents whose translog append completed always survive (replayed on restart), no matter whether their segment was written. The only way to lose a write is to crash in the microseconds before the journal fsync, which is why the client's acknowledgment waits for it.

Immutability pays dividends everywhere: it is why replication and snapshots are cheap, and why the query engine can parallelize by segment.