TTL & Row Settings
A TTL (time-to-live) puts an age bound on rows: declare it once when a store is created, and the engine removes rows that have not been written to for the given duration. Where a ring buffer bounds data by count, a TTL bounds it by age - sessions, rate-limit windows, caches - without a cleanup job or a delete query anywhere. The TTL lives in the row settings block, alongside a persistent switch for rows that should never reach disk at all.
Declaring a TTL
Row settings are part of the with block at creation: with { row: { ttl: { duration: '30m', mode: drop } } }. The duration is required and must be positive; it is a quoted duration literal such as '45s', '30m', '2d', or a compound like '2d12h'. mode is optional and defaults to drop, the only mode the collector currently executes (more on delete below). Run the snippets on this page in order:
The same row block is accepted wherever rows are stored: tables, series, ring buffers, and views. Row settings are fixed at creation - there is no alter for them.
The clock starts at the last write
A row's age is measured from its last committed write, not from a timestamp column you maintain. Every commit is stamped with a version, and the engine keeps a mapping from wall-clock time to commit versions; a row expires when its newest version is older than the TTL. That makes TTL an inactivity bound: updating a row - any column, any value - restarts its clock. A session stays alive as long as it is touched, and quietly ages out once it is not:
Because expiry is anchored to the write itself, there is no per-row expiry column to declare and nothing to keep in sync. The engine rejects any attempt to anchor the TTL elsewhere:
Expiry is a background sweep
Expired rows are collected by a background scan, not at read time. The collector wakes on an interval (60 seconds by default, the ROW_TTL_SCAN_INTERVAL engine setting), walks each store that declares a TTL in batches, and physically removes every version of each expired row - reclaiming the storage, from memory and disk alike.
The consequence: a TTL is a retention guarantee, not a precise visibility deadline. A row past its TTL can remain readable until the next sweep picks it up. If a query must never see rows older than some cutoff, keep a timestamp column and filter on it - and let the TTL handle the physical cleanup behind it.
What expiry means for views
drop mode is silent by design. Expired rows are removed from storage directly - no delete events are emitted, nothing flows through CDC, and views and subscriptions over the store are not notified. A view built over a TTL table keeps the rows it has already derived even after the source rows expire.
That is not a gap to work around - it is how derived retention is meant to be expressed. Views accept the same row settings, so derived state declares its own lifetime, independent of its sources:
mode: delete for expiry that emits real deletes - removals that would flow through incremental view maintenance and CDC like any other write. The current collector skips stores configured with delete entirely, so declaring it today means no cleanup happens at all. Use drop.Memory-only rows
The second row setting is persistent. It defaults to true; set it to false and the store's rows are never flushed to the persistent tier - they live in memory only and do not survive a restart. This is the shape for state that is worthless tomorrow anyway: rate-limit counters, presence, short-lived coordination state. Writes stay transactional and queries work as usual; the rows just never cost any disk:
A non-persistent store must declare a TTL - rows that never reach disk and never expire would only ever grow. The engine enforces the pairing:
TTL for operator state
Stateful operators inside a view query accept a TTL of their own: apply, distinct, and append take with { ttl: { duration: '1h' } }, and a join scopes it per side with with { ttl: { left: { duration: '1h' }, right: { duration: '1d' } } }. This bounds the operator's internal state - join entries, distinct keys - the same way a row TTL bounds a store: entries idle longer than the duration are evicted by the background collector. Operator TTL is always silent (only drop is allowed), so evicting state emits nothing downstream; it means the operator simply forgets inputs older than the TTL.
TTL or something else?
- --Bound by age: TTL. Bound by count: a ring buffer.
- --Bound by both: they compose. A ring buffer with a row TTL caps the row count and drops entries that go stale before eviction reaches them.
- --Removal that must be observed - audited, mirrored into views the moment it happens: an explicit
delete, which is an ordinary transactional write.
drop mode - so the system's own append-only telemetry is bounded by the same mechanism available to your data.