Declare a ttl_field on any entity with a timestamptz not null column, and atlantis deletes expired rows every five minutes.
1. Add the TTL column + directive
entity Session in consumer {
id varchar(8) primary
consumer_id varchar(8) not null references consumer.Account.id
session_token varchar(255) not null unique
expires_at timestamptz not null
created_at timestamptz not null default now()
ttl_field expires_at
}ttl_field names the column the sweeper compares against now(). Declare it timestamptz not null — the apply does not check the column’s shape, and a sweep against any other shape matches nothing.
2. Apply
tide applyThe ttl_field directive is recorded in the IR checkpoint. The built-in SweepExpired job reads the checkpoint at runtime to discover which entities have TTL columns.
Verify
Create a row whose expires_at is already in the past — through your
generated client, or any write path your service has. Within five minutes
the sweeper deletes it: a Query with an expires_at upper bound of the
current time returns nothing.
How it works
- atlantis ships a built-in job
atlantis.SweepExpiredthat runs on a*/5 * * * *cron schedule (every five minutes). - On each fire, the sweeper loads the IR checkpoint, finds every entity with
ttl_fieldset, and deletes up to 1000 expired rows per entity. - The batch limit bounds how long the sweep holds a lock; leftover rows are deleted on the next sweep.
- A sweep that fails surfaces to atlantis operators, not in your caller’s dead-letter queue. If expired rows persist across several sweep intervals, contact atlantis support.
Expiring rows on a tenant-isolated table
A DELETE sweep cannot work on a table with tenant isolation. The sweeper is a background job with no request behind it, so it binds no tenant; row-level security still applies to its DELETE, atlantis.current_partition() is NULL, and the statement matches nothing.
Declare the entity a hypertable on its TTL column, and expiry drops whole chunks instead:
hypertable Event in shop on occurred_at {
id bigint not null
tenant varchar(32) not null
occurred_at timestamptz not null
body text
primary by id, occurred_at
chunk_time_interval 1d
partition by tenant
ttl_field occurred_at
}The primary key includes occurred_at because TimescaleDB refuses a unique index that does not contain the time column. A single-column id primary fails the apply with SQLSTATE TS103.
Dropping a chunk is DDL, and row-level security filters queries, not DROP TABLE, so this needs no tenant bound. Chunk drops are one operation per chunk rather than one per row.
The TTL column must be the time dimension
ttl_field has to name the same column the hypertable is declared on. Chunks are selected by the time dimension, so if ttl_field named a different column a chunk whose time range has passed could still hold rows whose TTL has not — and dropping it would delete live data.
tide apply refuses any other combination of partition by and ttl_field. The error names all three ways out: declare the hypertable, drop partition by, or expire from your caller.
Granularity
A chunk is dropped only once its entire time range is in the past, so rows can outlive their TTL by up to one chunk_time_interval. Choose the interval for the retention precision you need — 1d means a row expires within a day of its TTL, 1h within an hour.
Entities the sweeper skips
The sweeper skips a ttl_field on a tenant-isolated entity that is not a
hypertable. tide apply refuses that declaration today, so the state
exists only on schemas recorded before the refusal; if you have one,
redeclare the entity as a hypertable on its TTL column.
Related
- Jobs and workflows concept. The sweeper is itself a job running on the atlantis runtime.
- Declarative jobs guide. How to declare and run your own jobs.