Architecture
The single-node request flow, from gRPC/REST down to the mmap snapshot.
Single-node request flow
gRPC (:9090) ──┐
├─▶ internal/api/grpc + internal/api/gateway (REST/JSON facade)
REST (:8080) ──┘
│
▼
internal/engine.Store (tenant routing, API-key scope, quotas, rate limits)
│ (one Engine opened lazily per tenant, closed when idle)
▼
internal/engine.Engine
│
┌───────────┴────────────┐
│ WAL (fsync before ack,│
│ group commit) ──▶ HNSW│
│ graph (in-memory) │
└───────────┬────────────┘
▼
internal/storage/segment (periodic mmap-backed snapshot)Every write is WAL-logged and fsync'd before it touches the graph. Concurrent writes share one fsync (group commit), and a batch upsert fsyncs once for the whole batch. Periodic snapshots let a restart skip replaying the WAL from empty; they are written without blocking writes, and a mostly-deleted graph is compacted when snapshotted. See WAL then snapshot for what that durability guarantee costs.
The pieces
internal/api/grpc+internal/api/gateway: the gRPC service and a hand-written REST/JSON translation layer over it. Notgrpc-gateway, since vendoring the full googleapis proto tree isn't worth it for a dozen routes. Optional API keys (internal/auth) and TLS sit in front of both.internal/engine.Store: routes requests to the right tenant, and enforces storage quotas (max vectors) and rate limits (max QPS) before a request reaches the engine. OneEngineis opened lazily per tenant, on first access, and closed again after-tenant-idle-timeout.internal/engine.Engine: owns one tenant's WAL, HNSW graph, and metadata index. Metadata filters use per-key postings, and a selective filter is answered by exact scoring instead of the graph.internal/index/hnsw: Malkov and Yashunin's Hierarchical Navigable Small World graph, from scratch, with the paper's diversity heuristic for neighbor selection and a parallel batch build. Searches share a read lock; an insert finds its neighbors without the write lock and takes it only to link in. Verified undergo test -race.internal/index/pq: lossy compression, using k-means++-trained codebooks per subspace and asymmetric distance computation (ADC), with re-ranking and an IVF variant. A tested package thatnucladbddoesn't use yet.internal/storage/wal: fsync-before-ack, a custom binary record format with CRC32 checksums, and torn-write-safe replay that refuses to silently skip corruption in the middle of the log.internal/storage/segment: atomic (write-to-temp, rename-into-place) full-graph snapshots, loaded via mmap so a dataset larger than RAM can page in via the OS instead of failing.
Multi-tenancy
Every tenant gets a fully isolated graph, WAL, and snapshot on disk, with its own dimension, distance metric, storage quota (max vectors) and rate limit (max QPS). Quotas are enforced before a request reaches the engine, and all of it persists across restarts. The CLI reference shows this in a real recorded session: the same vector id existing independently, with different data, under two different tenants.
Observability
- OpenTelemetry tracing: every gRPC call gets a trace span. Export is off by default so the hot path doesn't pay for spans nobody reads; set
OTEL_EXPORTER_OTLP_ENDPOINTto send them to a collector, orNUCLADB_TRACE_STDOUT=1to print them. - Prometheus metrics: request-count/duration histograms (raw histograms, not pre-baked percentiles), plus per-tenant quota-usage gauges, served at
/metrics.
For the distributed layer built on top of this single-node engine, see Distributed cluster.