---
title: "Expire rows automatically"
description: "Declare a ttl_field on any entity with a timestamptz not null column, and atlantis deletes expired rows every five minutes."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.tryatlantis.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Expire rows automatically

Declare a `ttl_field` on any entity with a `timestamptz not null` column, and atlantis deletes expired rows every five minutes.

> **Prerequisites**
>
> - A caller set up with `tide` ([Get started](/getting-started/)).

## 1. Add the TTL column + directive

```atl
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

```bash
tide apply
```

The `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.SweepExpired` that runs on a `*/5 * * * *` cron schedule (every five minutes).
- On each fire, the sweeper loads the IR checkpoint, finds every entity with `ttl_field` set, 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:

```atl
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](/concepts/jobs-and-workflows/). The sweeper is itself a job running on the atlantis runtime.
- [Declarative jobs guide](/guides/declarative-jobs/). How to declare and run your own jobs.

Source: https://docs.tryatlantis.dev/guides/row-ttl/index.mdx
