Backup & Restore
RelataDB's durability model is object-store-native. The bucket is the backup. Snapshots are open-format Parquet and Arrow IPC files readable by DuckDB, Spark, or Athena even without RelataDB running. There is no proprietary backup format.
How durability works
- Every write appends to the WAL before the ack is returned.
- On graceful shutdown, the WAL is flushed and a Parquet snapshot is written to the object store.
- On restart, the node replays the manifest catalog (lazy restart) or all segments (eager restart).
- Hash-chained commit manifests make any tampering detectable.
A SIGKILL skips the Parquet snapshot — the WAL is still intact and replayed on next boot. This takes longer than a clean restart but data is not lost.
Wire the object store
Without an object-store endpoint, RelataDB writes to RELATA_DATA_DIR/objects on local disk. That is fine for dev but not for production. Configure S3-compatible object storage:
export AWS_ENDPOINT_URL=http://minio:9000 # or real S3: https://s3.amazonaws.com
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
export AWS_REGION=us-east-1 # required for real S3
export RELATA_S3_BUCKET=relata-backupsVerify the config before starting the server:
relata check
# Prints: object_store: ok wal: ok chain: valid ...For GCS or Azure Blob, the same AWS_ENDPOINT_URL pattern works via the object-store compatibility layer:
# GCS via S3 compatibility
AWS_ENDPOINT_URL=https://storage.googleapis.com \
AWS_ACCESS_KEY_ID=GOOGXXXXXXXXXXXXXXXX \
AWS_SECRET_ACCESS_KEY=xxxxxx \
RELATA_S3_BUCKET=my-gcs-bucket \
relata serve
# Azure Blob via azurite (local) or production endpoint
AWS_ENDPOINT_URL=http://azurite:10000/devstoreaccount1 \
AWS_ACCESS_KEY_ID=devstoreaccount1 \
AWS_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== \
RELATA_S3_BUCKET=relata \
relata serveMinIO validation recipe
Run this end-to-end before any production deployment to confirm object-store wiring is correct.
# 1. Start MinIO
docker run -d --name minio -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minio \
-e MINIO_ROOT_PASSWORD=minio123 \
minio/minio server /data --console-address ":9001"
# 2. Wait for MinIO to be healthy
until curl -sf http://localhost:9000/minio/health/live; do sleep 1; done
# 3. Set object-store env
export AWS_ENDPOINT_URL=http://localhost:9000
export AWS_ACCESS_KEY_ID=minio
export AWS_SECRET_ACCESS_KEY=minio123
export RELATA_S3_BUCKET=relata-test
# 4. Verify config
relata check
# 5. Start server and ingest data
RELATA_PROFILE=server RELATA_BEARER_TOKEN=test relata serve &
sleep 3
curl -X POST http://localhost:9090/ingest \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"object_type":"Person","data":[{"name":"Ada"},{"name":"Alan"}]}'
relata query "SELECT * FROM Person LIMIT 5"
# 6. Graceful shutdown — flushes WAL + writes Parquet snapshot
kill -TERM $(pgrep -f "relata serve")
sleep 5
# 7. Restart — should reload from object store
RELATA_PROFILE=server RELATA_BEARER_TOKEN=test relata serve &
sleep 3
# If this returns rows, durability is working
relata query "SELECT * FROM Person LIMIT 5"
# 8. Backup and restore
relata backup
relata reset reset reset # triple "reset" is a safety guard
relata restore ~/.relata/store-*.json
relata query "SELECT * FROM Person LIMIT 5"
# Cleanup
pkill -f "relata serve"
docker rm -f minioStep 7 is the critical check. Zero rows after restart means the object-store wiring is wrong — fix it before deploying.
MinIO docker-compose
For a persistent local MinIO setup alongside RelataDB:
services:
relata:
image: ghcr.io/relatadb/relata:latest
restart: unless-stopped
ports:
- "9090:9090"
environment:
RELATA_PROFILE: server
RELATA_BEARER_TOKEN: "change-me"
RELATA_LAZY_RESTART: "true"
RELATA_LOG_FORMAT: json
AWS_ENDPOINT_URL: "http://minio:9000"
AWS_ACCESS_KEY_ID: minio
AWS_SECRET_ACCESS_KEY: minio123
RELATA_S3_BUCKET: relata-data
depends_on:
minio:
condition: service_healthy
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio123
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 3s
retries: 5
volumes:
minio-data:Graceful shutdown flow
SIGTERM triggers an ordered shutdown:
- Stop accepting new requests.
- Drain in-flight queries (bounded timeout).
- Flush the WAL.
- Write a final Parquet snapshot to the object store.
- Close listeners and release the
relata.locksingleton.
# Graceful shutdown
kill -TERM $(pgrep -f "relata serve")
# Force shutdown (WAL intact, Parquet snapshot skipped)
kill -9 $(pgrep -f "relata serve")Always use SIGTERM in production. Kubernetes terminationGracePeriodSeconds should be at least 30 to allow the WAL flush to complete.
Per-write durability levels
Control durability per-write with the X-Relata-Durability header:
# Default: async — WAL durable, fsync deferred ~10ms
curl -X POST http://localhost:9090/ingest \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
-H "X-Relata-Durability: async" \
-H "Content-Type: application/json" \
-d '{"object_type":"Person","data":[{"name":"Ada"}]}'
# Sync: fsync before ack — RPO = 0
curl -X POST http://localhost:9090/ingest \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
-H "X-Relata-Durability: sync" \
-H "Content-Type: application/json" \
-d '{"object_type":"Person","data":[{"name":"Alan"}]}'| Header value | fsync timing | Power-loss RPO |
|---|---|---|
async (default) / batch / interval | Background flusher (~10 ms) | ≤ 10 ms of acked writes |
sync / always / rpo0 | Before ack | 0 (no data loss) |
Use sync for financial or compliance writes where you need RPO = 0. Use async (default) for bulk ingestion.
PITR via object-store versioning
Enable versioning on your S3 bucket to get point-in-time recovery for free — each Parquet segment flush writes a new object version. To restore to a point in time:
# List object versions (AWS CLI example)
aws s3api list-object-versions \
--bucket relata-backups \
--prefix relata/ \
--query 'Versions[?LastModified>=`2026-07-13T00:00:00`]'
# Download a specific version
aws s3api get-object \
--bucket relata-backups \
--key relata/manifest.json \
--version-id "xxxxxx" \
manifest-pitr.jsonThen restore with the downloaded manifest:
relata reset reset reset
relata restore ./manifest-pitr.json
relata checkCold-restart RTO
| Scenario | Time at 10 M rows |
|---|---|
| WAL + Parquet flush on SIGTERM | ~15 s |
Cold restart (eager, RELATA_LAZY_RESTART=false) | ~55 s |
Cold restart (lazy, RELATA_LAZY_RESTART=true) | O(manifest) — seconds |
| Single-node RTO with lazy restart | <10 s to ready |
RELATA_LAZY_RESTART=true is the default on server and cluster. It loads only the manifest catalog on startup — row data hydrates on demand. Set RELATA_HYDRATE_RECENT_SEGMENTS=5 to pre-warm the 5 most recent segments for hot-path queries while staying mostly lazy.
Singleton enforcement
relata serve acquires an exclusive flock on data_dir/relata.lock. A second process mounting the same data directory fails immediately with a clear error rather than silently corrupting data. In Kubernetes, use a StatefulSet with accessMode: ReadWriteOnce to prevent two pods from racing on the same PersistentVolumeClaim.
See also
- Configuration — storage and cold-load env vars
- Scaling — lazy restart, RAM walls, paged backends
- Observability — WAL health metrics, audit chain