DRAFTThis page is not published. Only visible in development mode.

Ring Buffers

A ring buffer is a fixed-capacity store: when it is full, inserting a new row evicts the oldest one. It is the shape for "keep the last N" state - recent activity, latest readings, bounded logs - without a cleanup job, a cron task, or a delete query anywhere.

The last N rows, per key

Capacity is declared at creation with with { capacity: N }. Adding partition_by turns one buffer into an independent buffer per distinct key combination - "the last N events per region" or "the last N actions per user" in a single object. Here each region keeps its own two newest events: the third east insert evicts the first, while west is untouched. Run the snippets on this page in order:

$ One Buffer per Partition
$ ctrl+enter to run

Eviction, step by step

An unpartitioned buffer is one queue. Filling it to capacity keeps everything:

$ Create a Ring Buffer and Fill It to Capacity
$ ctrl+enter to run

One insert past capacity and the oldest row is gone. The write succeeds normally; eviction is a property of the store, not an error:

$ One More Insert Evicts the Oldest Row
$ ctrl+enter to run

Rows stay mutable

Unlike a log, a ring buffer's rows are ordinary rows. You can update them in place and delete them ahead of eviction, with the same filter semantics as tables:

$ Ring Buffer Rows Are Mutable
$ ctrl+enter to run
$ Delete Without Waiting for Eviction
$ ctrl+enter to run

Ring buffer or something else?

  • --Bound by count: ring buffer. Bound by age: a table or series with a TTL.
  • --Unbounded, time-ordered history: a series.
  • --Derived state that should keep only its newest N rows: a ring-buffer-backed view gives eviction semantics to computed data too.
Everything is transactional
Inserts, evictions, updates, and deletes on ring buffers participate in the same transactions as every other write. A view over a ring buffer sees evictions as ordinary deletes.