Storage & Cluster
Storage paths, WAL, HNSW shape, scheduler intervals, blob backends, cold backups, and the experimental cluster mode.
CortexDB stores everything on one canonical write path: an append-only WAL backed by RocksDB. Every searchable index — HNSW vectors, Tantivy fulltext, the KG, materialized views — is a derivative of that WAL. Restore the WAL and you can rebuild every index.
Storage paths and durability
[storage]
data_path = "/data/cortex" # default
wal_sync = true # default
max_disk_usage_percent = 95 # default| Field | Type | Default | What it controls |
|---|---|---|---|
data_path | PathBuf | /data/cortex | Root for RocksDB, Tantivy, and the WAL. |
wal_sync | bool | true | fsync() after every WAL append. false = ~10 ms durability gap, ~2–5× faster writes. |
max_disk_usage_percent | u8 [0..100] | 95 | Soft refuse-writes watermark. At this fullness /v1/experience returns 507. |
The default /data/cortex suits the Docker/k8s shape (mount a real volume there); bare-metal usually
overrides to /var/lib/cortexdb. The config file lives at <data_path>/cortex.toml, so changing
data_path also moves the config. wal_sync = false (or CORTEX_WAL_SYNC=0) is the explicit
faster/weaker opt-out — fine for voice/realtime, not for financial/medical/compliance workloads.
max_disk_usage_percent is a soft floor (RocksDB itself doesn't refuse writes) — keep ~10% headroom on
the actual disk.
The extraction cache
Alongside the stores, the data directory holds an extraction cache that lets repeated extraction
work skip the model call. It is on by default and lives at <data_dir>/extraction_cache (verified
present on a fresh v0.9.9 boot). Four environment variables control it:
| Env var | Default | What it controls |
|---|---|---|
CORTEX_EXTRACTION_CACHE | (unset = on) | Set =0 to disable; call sites then send no cache key. |
CORTEX_EXTRACTION_CACHE_DIR | <data_dir>/extraction_cache | Where the on-disk tier lives. |
CORTEX_EXTRACTION_CACHE_RAM_CAP | 4096 entries | In-memory tier size; 0 disables the RAM tier. |
CORTEX_EXTRACTION_CACHE_TTL_DAYS | 14 (range 1–365) | Entry lifetime. Expiry is lazy, on read. |
Because expiry is lazy, the directory can hold entries past their TTL until they are next read — size the volume for the cache as well as the stores.
Engine: HNSW and cache
[engine]
max_memory_bytes = 25769803776 # 24 GB (default)
vector_dimensions = 3072 # default; MUST match embedding output
hnsw_m = 16 # default
hnsw_ef_construction = 200 # default
hnsw_ef_search = 100 # default; runtime-tunable
hnsw_quantization = "tq2" # default TQ2; opt out with sq8 / fp32
hnsw_tombstone_rebuild_threshold = 0.15 # default; 15% deleted → rebuild
block_cache_bytes = 8589934592 # 8 GB (default)| Field | Default | Range | Tuning |
|---|---|---|---|
max_memory_bytes | 24 GB | ≥ 4 GB | Engine heap budget. Physical RAM minus 4–8 GB. |
vector_dimensions | 3072 | {256, 384, 512, 768, 1024, 1536, 3072} | Must equal the embedding output dim. |
hnsw_m | 16 | [4, 64] | Edges per node. Higher = better recall, larger/slower build. |
hnsw_ef_construction | 200 | [10, 2000] | Build candidate pool. |
hnsw_ef_search | 100 | [10, 2000] | Query candidate pool. Higher = better recall, slower queries. |
hnsw_quantization | tq2 | tq2 / sq8 / fp32 | sq8 = 1 byte/dim (~4× memory savings, ~-0.5 pp recall); fp32 keeps full precision. |
hnsw_tombstone_rebuild_threshold | 0.15 | [0.0, 1.0] | Rebuild when 15% of nodes are deleted. |
block_cache_bytes | 8 GB | — | RocksDB block cache; affects cold-data read latency. |
The quantization default is TQ2, not ScalarU8
config_lint reports CORTEX_HNSW_QUANTIZATION = tq2 (unset = TQ2; opt out with sq8/fp32), and
GET /v1/admin/metrics shows the running indexes at quantization: "TQ2" (main) / "SQ8" (entity).
The accepted values are tq2 / sq8 / fp32 — ScalarU8 / None are not the engine's tokens.
HNSW recipes
| Goal | hnsw_m | ef_construction | ef_search | Quantization |
|---|---|---|---|---|
| Default (LongMemEval-S 93.8%) | 16 | 200 | 100 | tq2 |
| Voice / realtime | 16 | 200 | 60 | tq2 |
| Memory-constrained | 16 | 200 | 100 | sq8 |
| Max recall accuracy | 32 | 500 | 200 | fp32 |
| Bulk ingest (build fast) | 16 | 100 | 100 | tq2 |
The Max-Recall config buys ~+0.5–1 pp at ~3× memory, ~2× build, ~2× query — rarely worth it in production.
Vector index persistence and memory
The HNSW index is snapshotted to the data directory so a restart warms from disk instead of rebuilding.
These are environment variables, not cortex.toml fields; every default below is the value
config_lint reports on v0.9.9.
| Env var | Default | What it controls |
|---|---|---|
CORTEX_SNAPSHOT_INTERVAL_SECS | 300 | How often an index snapshot is written. |
CORTEX_SNAPSHOT_KEEP_PREV | 1 | Previous generations kept alongside the current one. |
CORTEX_SNAPSHOT_WRITE_MBPS | 256 | Snapshot write throttle; 0 = unthrottled. |
CORTEX_VECTOR_RESIDENT_MAX | (unset = uncapped) | Hot-tier watermark: how many vectors stay resident in memory. |
CORTEX_EVICT_VECTORS_ON_ENRICH | (unset = off) | Demote vectors during enrichment, for steady-state memory. |
CORTEX_VECTOR_SHARDS | 8 | HNSW shard count. |
CORTEX_SHARD_SEARCH_THREADS | 8 | Per-query chunk fan-out across shards. |
CORTEX_SHARD_SEARCH_POOL | available cores (cap 64) | Search thread-pool size. |
CORTEX_DB_WRITE_BUFFER_MB | 256 (512 for the v1 WAL store) | Per-store RocksDB memtable budget. |
CORTEX_TOTAL_WRITE_BUFFER_MB | (unset = per-store caps only) | Shared memtable budget across stores. |
These snapshots are not backups
CORTEX_SNAPSHOT_* controls vector-index persistence, so that a restart can warm from disk instead
of rebuilding the index. It is not a backup: it captures the index rather than the data directory, and
it is not a recovery point. The supported backup path is the verified cold backup below.
The boot log warns when the hot tier is uncapped
With CORTEX_VECTOR_RESIDENT_MAX unset the hot tier is uncapped, and config_lint computes what the
booting machine can actually hold — reporting bytes-per-vector at the active quantization against
available memory, and recommending a watermark for any corpus that can outgrow it. Set that watermark
(and consider CORTEX_EVICT_VECTORS_ON_ENRICH) before the corpus reaches that size. See
Reading the config_lint dump.
Network and ports
[network]
api_port = 8443 # cortex.toml default
gossip_port = 7000 # default
grpc_port = 9042 # default
request_timeout_ms = 10000 # default (10 s)
gossip_interval_ms = 1000 # default (1 s)| Port | Role | Notes |
|---|---|---|
3141 | v1 API + admin UI (single-node CLI default) | The one public port. |
8443 | api_port from cortex.toml | Cluster-mode binaries (experimental). |
7000 | UDP gossip | Cluster membership (experimental). |
9042 | Internal gRPC RPC | Inter-node calls (experimental). |
Port-defaults gotcha
The single-node CLI defaults --port=3141, while the TOML api_port defaults to 8443. Pick the
value your reverse proxy forwards to and set both consistently.
Scheduler
[scheduler]
enabled = true # default
compaction_interval_secs = 300 # 5 min (min: 30)
methylation_interval_secs = 600 # 10 min (min: 60)
enrichment_drain_interval_secs = 30 # 30 s (min: 5)
cognitive_persist_interval_secs = 60 # 1 min (min: 10)
feedback_weight_interval_secs = 120 # 2 min (min: 30)| Job | Default | What it does |
|---|---|---|
| Compaction | 5 min | Merge/dedupe entries; reduce footprint, improve recall over time. |
| Methylation | 10 min | Decay-adjust salience by access patterns. |
| Enrichment drain | 30 s | Consume async LLM extraction results. |
| Cognitive persist | 1 min | Checkpoint planner state + ranker weights. |
| Feedback weight | 2 min | Apply feedback gradients to ranker weights. |
CORTEX_SCHEDULER_DISABLE=1 disables the whole scheduler at startup (always set for benchmarks). Schema
validation enforces the minimum intervals — compaction_interval_secs = 10 fails startup.
Cluster topology (experimental — not operational)
Cluster mode does not provide fault tolerance today
Single-node (cortexdb [PORT] [DATA_DIR]) is the supported deployment, production-ready for
≤ 10M events. Cluster mode (all four of --node-id, --rpc-addr, --gossip-addr, --seed-nodes
together) is under development — multi-node replication and HA do not work; nodes run as
independent databases. A "3-node, rf=3" deployment does not survive a node loss. Do not deploy
cluster mode for redundancy, failover, or capacity.
cortexdb \
--node-id=1 \
--rpc-addr=10.0.0.1:7100 \
--gossip-addr=10.0.0.1:7000 \
--seed-nodes=10.0.0.1:7000,10.0.0.2:7000,10.0.0.3:7000 \
--rf=3 \
--port=3141 \
--data-dir=/data/cortex/node1[cluster]
node_id = 1 # must match --node-id
seed_nodes = ["10.0.0.1:7000", "10.0.0.2:7000"]
replication_factor = 3
vnodes_per_node = 256
consistency_default = "Quorum" # "One" | "Quorum" | "All"These fields describe the intended design; none of the replication or consistency settings provide fault tolerance in the current release.
Blob storage
Binary content (images, audio, video, documents) is stored in a blob backend with a content-addressed reference in the WAL.
[blob_store]
provider = "local" # "local" | "s3" | "gcs" | "azure"
# Local mode
data_dir = "/data/cortex/blobs"
# S3 mode
bucket = "acme-cortex-blobs"
region = "us-east-1"
endpoint = "" # optional — S3-compatible (R2, MinIO, B2)
access_key_id = "" # falls back to AWS_ACCESS_KEY_ID env
secret_access_key = "" # falls back to AWS_SECRET_ACCESS_KEY env
allow_http = false # true ONLY for MinIO over LAN
virtual_hosted_style_request = true # false for path-style URLs
# S3 encryption
s3_encryption_type = "aws:kms" # "AES256" | "aws:kms" | "" (none)
s3_kms_key_id = "arn:aws:kms:..." # required if s3_encryption_type = aws:kms
s3_bucket_key_enabled = true # KMS bucket key — saves KMS API costsEvery TOML field has an env-var form (e.g. CORTEX_BLOB_BUCKET, CORTEX_BLOB_S3_KMS_KEY_ID); the env
var wins if both are set. GCS uses gcs_bucket + gcs_application_credentials; Azure uses
azure_account + azure_container + azure_access_key (falls back to AZURE_STORAGE_KEY); MinIO is
S3 with endpoint + allow_http = true + virtual_hosted_style_request = false.
Blobs are referenced from /v1/experience payloads by content hash and served via
/v1/blobs/{id}. The server reads/writes the backend transparently —
co-locate CortexDB and the blob backend in the same region/VPC.
Content modality processors
Each modality has its own extraction integration; all are optional — if unconfigured, ingest silently skips extraction for that modality.
# Image (GPT-4o vision)
export CORTEX_IMAGE_PROVIDER=openai
export CORTEX_IMAGE_API_KEY=$OPENAI_API_KEY
export CORTEX_IMAGE_MODEL=gpt-4o
# Audio (Whisper)
export CORTEX_AUDIO_PROVIDER=openai
export CORTEX_AUDIO_API_KEY=$OPENAI_API_KEY
export CORTEX_AUDIO_MODEL=whisper-1
export CORTEX_AUDIO_LANGUAGE=en # optional
# Video (ffmpeg keyframes → image processor)
export CORTEX_FFMPEG_PATH=ffmpeg
export CORTEX_VIDEO_KEYFRAMES_PER_MIN=6
# Document (OCR / PDF extraction — e.g. Tika)
export CORTEX_DOCUMENT_PROVIDER=...
export CORTEX_DOCUMENT_API_URL=...
export CORTEX_DOCUMENT_API_KEY=...The same fields can be set under [content_processors.image] / .audio / .video / .document /
.sensor. See Media ingestion.
Disk sizing
Rough rule of thumb on text-heavy workloads with text-embedding-3-small (1536 d, tq2 quantization):
| Events stored | Disk footprint | Memory (cache + HNSW) |
|---|---|---|
| 100 K | ~2 GB | ~1 GB |
| 1 M | ~15 GB | ~6 GB |
| 10 M | ~120 GB | ~40 GB |
| 100 M | ~1.1 TB | ~350 GB (cluster) |
Above ~10M events, plan a very large single box — clustering is experimental/not operational, so scaling out isn't an option yet. Mitigate the one-box risk with regular verified cold backups and infra-level redundancy (RAID, replicated volumes).
Backups and snapshots
The supported path is a verified cold backup of the data directory using scripts/cold_backup.py.
There is no online/hot backup, no scheduled job, no object-store upload target, and no PITR in the
current release — those are planned, separate work.
# Server must be STOPPED — the tool refuses a live data dir.
python scripts/cold_backup.py backup <data_dir> <archive.tar.gz>
# Recompute every file digest against the per-file SHA-256 manifest.
python scripts/cold_backup.py verify <archive.tar.gz>
# Restore into a FRESH directory only; refuses non-empty targets and tampered archives.
python scripts/cold_backup.py restore <archive.tar.gz> <fresh_target_dir>The archive captures the whole data directory byte-for-byte — WAL, RocksDB, Tantivy, HNSW, and
cortex.toml — with a per-file SHA-256 manifest. See
Backups & Disaster Recovery for the full procedure.
Next steps
- Configuration Foundations — file/env/CLI precedence
- Security & Compliance — encryption, TLS, RBAC, audit
- Profiles & Presets — the Batch profile for high throughput