Skip to content

Reads

Reads are served by the stream's owning node and answered from the fastest tier that holds the data. A consumer following the tail almost never touches the object store. A consumer replaying history streams out of it through a readahead cache.

Every read is bounded by the stream's confirm offset, the highest offset whose WAL upload has completed. A record becomes readable at the same moment it becomes durable, never before, so a reader cannot observe data that a crash could take back.

The tiers

A read at offset n walks down until something answers.

read at nbounded by confirm offsettail cacherecent records, per streamlog cachenot yet uploaded blocksblock cachepages of committed objectsobject storeGET with readaheadmissmissmiss

The tail cache holds the most recently appended records of each stream in memory on the serving node, so a consumer keeping up with a producer reads without entering the engine at all. Behind it, the log cache holds every record that has not yet been uploaded into committed objects, which covers readers that are seconds behind. Both are filled by the write path, not by reads.

The block cache is the cold path. It holds fixed-size pages of committed objects, fetched with one GET per page no matter how many readers wait on it, and evicted by LRU with a TTL. A sequential reader gets a readahead window that grows as it keeps reading, so replay throughput approaches raw object store throughput after the first few requests.

Locating the right objects requires no listing. The metadata state indexes every committed object by stream and offset range, so "objects covering stream S from offset a" is a bounded lookup in the node's local view.

Waiting at the tail

A consumer that has read everything should not busy-poll. A read that arrives at the confirm offset returns immediately with an empty body and an up-to-date marker, and the client can instead ask to wait.

With long polling the request parks on a per-stream waiter until new data is durable, the stream closes, or the timeout lapses, 25 seconds by default. The waiter is woken by the write path at the moment an append's WAL upload completes, so delivery latency is the durability latency and nothing more.

The same waiter feeds SSE. A client that accepts text/event-stream gets each new record as an event on a held-open response, with the connection recycled after 55 seconds so proxies and load balancers never see an idle connection they might kill. The client reconnects from its last offset without losing anything, because offsets, not the connection, track position.

Reading through a transfer

A read hitting a node that no longer owns the stream gets a 307 redirect to the current owner, as described in Ownership and routing. Offsets are stable across the move, so a consumer resumes on the new owner from exactly where it left off. The new owner's caches start cold, and the first reads after a transfer pay object store latency until the tail warms up again.