Skip to content

Data layout

Everything Fanout persists lives under FANOUT_DATA_DIR (./data by default, /var/lib/fanout/data in the container).

PathHolds
telemetry/parquet/batches/*.batch/Authoritative atomic batches containing metadata.json, present signal Parquet files, and trace.fidx when spans are present
telemetry/parquet/batches/_schema.batch/Empty schema anchors that keep every DuckDB view queryable
telemetry/parquet/batches/*.retired-*/Compaction inputs retained temporarily for crash rollback; a live marker protects its own set
telemetry/parquet/batches/*.quarantined-*/Unreadable batches explicitly set aside by fanout repair quarantine; preserved but not queryable
telemetry/parquet/staging/Unacknowledged writes being prepared for atomic publication
telemetry/compaction/Prepared replacement batches during compaction
telemetry/COMPACTION.jsonDurable marker for an interrupted compaction swap. Present only while one is unresolved, and blocks startup until it is — see troubleshoot
query/catalog.duckdbRebuildable DuckDB rollups and query state
query/tmp/Spill space for queries that exceed the memory cap
control/fanout.sqliteUsers, sessions, dashboards, alert rules, agent history

Each published Parquet batch directory is a self-contained crash-recovery unit. Its metadata records row counts and time bounds; its fixed-width trace sidecar maps hashes to sorted span row ranges. The sidecar is verified against the Parquet rows by offline repair, while DuckDB rollups can be rebuilt. Keeping the complete data directory is the supported and fastest restore path.

control/fanout.sqlite is independent of both in format but not in meaning: it holds the dashboards and alert rules that refer to the telemetry, and the accounts that own them.

So the unit of backup is the whole directory. Back up and restore covers doing that safely, which mostly means doing it while the process is stopped.

Telemetry is append-heavy, queried analytically over wide ranges, and never updated. Application state is small, transactional and updated constantly. Parquet with DuckDB serves the former; SQLite serves the latter — how storage works covers the reasoning and the write path.

DuckDB spills here when a query needs more memory than FANOUT_DUCKDB_MEMORY allows. It is regenerated as needed and holds nothing durable, but it does need space: a query against a wide time range can spill substantially, and a full disk fails the query rather than degrading it.

Retention is the main lever — FANOUT_RETENTION_DAYS, with expiry applied on the maintenance cycle rather than the moment you change it. File count matters as much as byte count for query latency, which is what the compaction pass exists to bound. Tune retention covers both.