Data layout
Everything Fanout persists lives under FANOUT_DATA_DIR (./data by default,
/var/lib/fanout/data in the container).
| Path | Holds |
|---|---|
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.json | Durable marker for an interrupted compaction swap. Present only while one is unresolved, and blocks startup until it is — see troubleshoot |
query/catalog.duckdb | Rebuildable DuckDB rollups and query state |
query/tmp/ | Spill space for queries that exceed the memory cap |
control/fanout.sqlite | Users, sessions, dashboards, alert rules, agent history |
Why it is one unit
Section titled “Why it is one unit”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.
Two engines, on purpose
Section titled “Two engines, on purpose”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.
query/tmp/
Section titled “query/tmp/”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.
Sizing
Section titled “Sizing”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.