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

  1. Every write appends to the WAL before the ack is returned.
  2. On graceful shutdown, the WAL is flushed and a Parquet snapshot is written to the object store.
  3. On restart, the node replays the manifest catalog (lazy restart) or all segments (eager restart).
  4. 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-backups

Verify 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 serve

MinIO 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 minio

Step 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:

  1. Stop accepting new requests.
  2. Drain in-flight queries (bounded timeout).
  3. Flush the WAL.
  4. Write a final Parquet snapshot to the object store.
  5. Close listeners and release the relata.lock singleton.
# 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 valuefsync timingPower-loss RPO
async (default) / batch / intervalBackground flusher (~10 ms)≤ 10 ms of acked writes
sync / always / rpo0Before ack0 (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.json

Then restore with the downloaded manifest:

relata reset reset reset
relata restore ./manifest-pitr.json
relata check

Cold-restart RTO

ScenarioTime 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