Skip to content

Retention

TaskOwl stores every Celery event in PostgreSQL as an append-only log. That is what makes task history, retry chains, and audits possible, but it also means the database grows without bound unless you prune it.

Retention is opt-in: all retention windows default to 0, which keeps everything forever (the 0.1.0 behavior).

Settings

Variable Description Default
EVENT_RETENTION_DAYS Delete task_events and worker_events older than this many days 0 (keep forever)
AUTOMATION_RUN_RETENTION_DAYS Delete automation_runs older than this many days 0 (keep forever)
RETENTION_CHECK_SECONDS How often the consumer runs a prune pass 3600
RETENTION_BATCH_SIZE Rows deleted per batch 10000
# Keep 14 days of task/worker events and 30 days of automation runs.
export EVENT_RETENTION_DAYS=14
export AUTOMATION_RUN_RETENTION_DAYS=30

Retention for events is measured against each event's own timestamp (event time), which reuses the existing timestamp indexes. Because that clock comes from the workers, a worker with a badly skewed clock can have its rows pruned earlier or later than you expect. Automation runs have no event timestamp and are measured against created_at (ingestion time).

How pruning runs

Pruning runs in the consumer on a timer (RETENTION_CHECK_SECONDS). When a retention window is enabled, each pass deletes expired rows in bounded batches (RETENTION_BATCH_SIZE) rather than one large DELETE, which keeps table locks short and avoids WAL bloat. If both retention windows are 0, the consumer logs that pruning is disabled and does no work.

Deleted rows leave dead tuples behind; PostgreSQL reclaims them on the next autovacuum, as usual. Nothing else is required.

Manual pruning

taskowl-prune runs a single prune pass and exits. It is useful for cron-based scheduling or one-off cleanups.

# Report how many rows would be deleted, without deleting anything.
taskowl-prune --dry-run

# Delete according to the configured windows.
taskowl-prune

# One-off override, regardless of EVENT_RETENTION_DAYS.
taskowl-prune --days 7

Running it from an external scheduler instead of the consumer is fine — just point it at the same database. Only one prune should be needed per window; overlapping runs are safe but redundant.

Duplicate events

Celery events are delivered at-least-once, so a broker redelivery or a consumer restart can re-send an event TaskOwl has already stored. Every event row carries a deterministic dedupe_key, and ingest uses INSERT ... ON CONFLICT DO NOTHING, so the same event never produces a second row.

Two caveats:

  • The key is derived from the event's type, task/host, and emitted timestamp. An event re-emitted with a different timestamp is not deduplicated. Deduplication reduces duplicates; it does not guarantee exactly-once delivery.
  • Events emitted without a timestamp cannot be keyed deterministically. They are stored with a NULL key and are always inserted.

Sizing

Row size depends on your task payloads (arguments, results, tracebacks). If you are unsure, start with a generous EVENT_RETENTION_DAYS and monitor the table sizes:

SELECT pg_size_pretty(pg_total_relation_size('task_events'));
SELECT pg_size_pretty(pg_total_relation_size('worker_events'));

High-frequency worker heartbeats are usually the first thing to cut back; they are the highest-volume event type and rarely need long retention.