Purple8 Deployment Model
The Embedded Engine Contract
Purple8 is an embedded multi-model database — the GraphEngine (backed by BrickCoreStorage and the Rust DashMap buffer) lives inside the serving process. It is not a network service that multiple processes connect to.
This is the same model as SQLite, LMDB, or DuckDB.
Multiple Workers: When It Works and When It Doesn't
uvicorn --workers N forks N separate OS processes. Each worker starts from the same on-disk state (WAL replay + BrickSlab restore at startup), so they all begin with identical data.
Startup: all workers replay WAL from disk → identical state ✓
Runtime reads: any worker can serve any query → fine ✓
Runtime writes: POST /nodes round-robins across workers
→ each worker's in-memory DashMap gets 1/N of the writes
→ reads on a different worker miss those writes ✗Works fine with N workers
- Read-heavy workloads after a completed ingest — all workers share the same on-disk baseline, all queries are served correctly
- Stateless API layers (auth, schema, metadata) — no in-memory write state
- Query, RAG, vector search, graph traversal — all read from the same BrickSlabs on disk
Breaks with N workers
- Concurrent HTTP ingest — if a client posts 32k nodes while 16 workers are running, each worker's in-memory DashMap receives ~2k nodes, and reads on any single worker return only that worker's subset until replay. Do not ingest via HTTP across multiple workers on a single instance — keep
WORKERS=1for write-enabled instances.
Hardware & Accelerator Auto-Discovery (worker_solver.py & CortexAutoConfig)
At startup, CortexAutoConfig and purple8_graph.core.worker_solver automatically inspect the host hardware and accelerator topology:
- Multi-socket CPU & NUMA topology: Detects
cpu_socketsandnuma_nodesviaHardwareTopologyand/sys/devices/system/node, computingrecommended_shards = max(cpu_sockets, numa_nodes)for NUMA-local shard pinning. - Intel AMX & Intel XPU: Detects Intel AMX (
amx_tile,amx_int8,amx_bf16) matrix extensions on Xeon Scalable processors and Intel XPU (torch.xpuintegrated and discrete Arc / Data Center GPU Max accelerators). - NVIDIA Tensor Core GPUs: Detects Hopper (
H100/H200,sm_90) and NVIDIA Blackwell / Blackwell Ultra B300 & GB300 (288 GB HBM3e, compute capabilitysm_103, 5th-gen FP4 Tensor Cores). - Apple Silicon Metal / Unified Memory: Detects Apple M-series unified memory (
mps) where CPU and GPU share physical RAM with zero PCIe copy overhead.
Run the worker solver directly to inspect your host's detected topology:
python -m purple8_graph.core.worker_solver --mode balanced --data-path /dataThe thread budget constraint
HardwareTopology allocates a full Rayon thread pool per worker process:
| Instance | Physical cores | Rayon threads/worker | WORKERS=4 total | WORKERS=16 total |
|---|---|---|---|---|
| c6i.4xlarge | 8 | ~9 | 36 threads | 144 threads |
| c6i.16xlarge | 32 | ~30 | 120 threads | 480 threads |
480 threads on 32 cores = 15× over-subscription. At WORKERS=16 on EC2 the OS scheduler was thrashing before any actual work happened.
Rule of thumb: WORKERS ≤ 4 on any instance type. Above 4, Rayon thread over-subscription dominates.
Purple8's parallelism comes from Rust, not from forking:
┌─────────────────────────────────────────────────────┐
│ Python process │
│ │
│ uvicorn (uvloop) ← async HTTP, no blocking I/O │
│ asyncio event loop ← handles N concurrent requests │
│ │
│ ThreadPoolExecutor (READ pool) ← from Rust HW │
│ ThreadPoolExecutor (WRITE pool) ← topology │
│ ThreadPoolExecutor (BULK pool) ← partition │
│ │
│ GraphEngine → BrickCoreStorage → Rust DashMap │
│ ↕ │
│ Rayon thread pool (all CPU cores) │
│ ├── WAL subsystem │
│ ├── Graph traversal │
│ ├── Vector search (HNSW) │
│ ├── Full-text search │
│ ├── Query planner │
│ └── Background compaction │
└─────────────────────────────────────────────────────┘HardwareTopology in the Rust core detects the available logical CPUs and divides them across 6 Rayon subsystem pools. A 32-vCPU machine gets ~5 cores for vector search, ~5 for graph traversal, ~4 for WAL, etc. — all inside a single Python process.
Configuration:
WORKERS=1 # always — the default in Dockerfile.commercial
P8G_HOST=0.0.0.0
P8G_PORT=8000Horizontal Scaling: Multiple Container Instances
To scale beyond one machine, run multiple container instances, each with its own data volume, behind a load balancer with consistent hashing (route by tenant, entity type, or key range):
┌── LB (consistent hash) ──┐
│ │
client ──────────┼── Instance A (vol-a) ────┤
│ engine owns shard A │
├── Instance B (vol-b) ────┤
│ engine owns shard B │
└── Instance C (vol-c) ────┘
engine owns shard CThis is true horizontal sharding — not in-process forking. Each instance is a fully independent Purple8 node. The supergraph/ module provides cross-instance query federation when needed.
For HA within a shard, use raft_cluster.py (Raft consensus, 3-node quorum) with a replica.py follower per node.
Primary / Replica Topology (Horizontal Read Scaling + HA)
Purple8 Graph runs as one primary and N replicas. The primary owns writes; replicas receive the WAL over a dedicated replication link (port 8101), apply it to their own storage, and serve reads independently. The same replicas are the failover candidates.
┌── LB: writes (POST/PUT/PATCH/DELETE) ──► Primary
client ──────────┤
└── LB: reads (GET/HEAD) ──► Primary + Replica 1 + Replica 2 ...
Primary ──WAL stream (8101, token/TLS)──► Replica 1
──WAL stream ─────────────────► Replica 2How it works:
- Primary runs with
P8G_REPLICATION_ROLE=primaryand lists its replicas inP8G_FOLLOWER_ADDRESSES.P8G_REPLICATION_MODE=syncmakes a write return only afterP8G_REPLICATION_SYNC_REPLICASreplicas have acknowledged it (RPO ≈ 0);asyncreturns immediately. - Replicas run with
P8G_REPLICATION_ROLE=followerandP8G_PRIMARY_HOST/PORT. Each has its own data volume — no shared filesystem is required or recommended. The full HTTP API is served; write verbs return503withRetry-After(follower_write_guard) so a mis-routed write fails closed. - Reads on replicas are eventually consistent;
GET /healthreportsreplication_lag(operations behind the primary). Route reads that need read-your-writes to the primary. - Failover (
P8G_FAILOVER_ENABLED=1on a replica): a watchdog probes the primary's/healtheveryP8G_FAILOVER_INTERVAL_S; afterP8G_FAILOVER_UNHEALTHY_THRESHOLDconsecutive failures it promotes itself viaPOST /replication/promote(authenticated with the sharedP8G_REPLICATION_AUTH_TOKEN) and repoints the load balancer through theP8G_FAILOVER_TARGET_BACKENDhook (elbv2on AWS;nooplogs only). A cooldown (P8G_FAILOVER_COOLDOWN_S) prevents flapping. With more than one replica, enable the Raft coordination backend so the election guard and STONITH fencing apply.
Configuration:
# Primary
P8G_DATA_PATH=/data/graph
P8G_REPLICATION_ROLE=primary
P8G_REPLICATION_BIND_HOST=0.0.0.0
P8G_REPLICATION_TLS=1
P8G_REPLICATION_TLS_CERT=/run/tls/primary.pem
P8G_REPLICATION_TLS_KEY=/run/tls/primary.key
P8G_REPLICATION_TLS_CA=/run/tls/ca.pem
P8G_REPLICATION_MODE=sync
P8G_REPLICATION_SYNC_REPLICAS=1
P8G_FOLLOWER_ADDRESSES=replica-1:8101
P8G_REPLICATION_AUTH_TOKEN=<shared secret>
WORKERS=1
# Replica
P8G_DATA_PATH=/data/graph # its own volume
P8G_REPLICATION_ROLE=follower
P8G_REPLICATION_BIND_HOST=0.0.0.0
P8G_REPLICATION_TLS=1
P8G_REPLICATION_TLS_CERT=/run/tls/replica.pem
P8G_REPLICATION_TLS_KEY=/run/tls/replica.key
P8G_REPLICATION_TLS_CA=/run/tls/ca.pem
P8G_PRIMARY_HOST=primary
P8G_PRIMARY_PORT=8101
P8G_REPLICATION_AUTH_TOKEN=<shared secret>
P8G_FAILOVER_ENABLED=1
P8G_FAILOVER_PRIMARY_ADDRESS=primary:8080
P8G_NODE_ADDRESS=replica-1:8080
P8G_FAILOVER_TARGET_BACKEND=noop # or elbv2 + P8G_FAILOVER_WRITE_TG_ARN / READ_TG_ARN
WORKERS=1Replication requires a shared authentication token and TLS before network threads start. Cross-host listeners must opt into a non-loopback bind address. Promoted replicas retain their authentication, TLS and synchronization settings. Automated promotion requires verified HTTPS on the advertised API addresses (directly or through the existing TLS ingress) and does not follow redirects. See Clustering for the explicit loopback-only development exception.
P8G_READONLY=1 is a separate, static mode: the instance rejects all writes for its lifetime regardless of replication role. Use it for a standalone read-only copy restored from backup; do not combine it with P8G_REPLICATION_ROLE=follower (the follower guard already covers writes and promotion must be able to lift it).
Constraints:
- One primary per logical database. Write throughput scales vertically; read throughput scales with replica count.
- Read-your-writes is not guaranteed on replicas. Route
/auth/*,/replication/*,/mcp*,/lcnc*and SSE change feeds to the primary. - Promotion is irreversible within the process lifetime; the old primary must be restarted as
followerto rejoin.
When to use:
- Read-heavy workloads that exceed one instance
- Two-AZ / two-host availability with automated failover
- Analytics queries that would otherwise contend with writes
Reference deployment: deployment/cloudformation/aws/purple8-aml-instance.yaml (EC2 + EBS primary and replica, method-based ALB routing, ELBv2 failover hook).
uvloop + httptools
uvicorn[standard] installs:
- uvloop — Cython-based libuv event loop; 2–4× faster than asyncio's default
SelectorEventLoopfor I/O-bound workloads - httptools — fast LLHTTP-based HTTP/1.1 parser (same parser Node.js uses)
These are enabled automatically when installed. The entrypoint passes --loop uvloop --http httptools explicitly so the flags are visible in process listings and container logs.
Performance Landmarks (c6i.16xlarge, 32 vCPU)
| Operation | Time | Notes |
|---|---|---|
count_nodes() | 0.006 ms | O(1) Rust DashMap atomic counter |
get_nodes_page(limit=10) | 0.052 ms | Storage push-down, O(limit) |
iter_nodes()[:10] (old path) | 34.2 ms | Full scan, O(N) — 658× slower |
| Middleware stack (9 layers) | 0.06 ms | HMAC, RBAC, tenant, telemetry |
| BrickWAL fdatasync (1 batch) | ~7 µs | Per the lock-free write path |
The list_nodes HTTP handler serialises via raw orjson.dumps() (skips FastAPI response model re-serialisation) and returns a plain Response with media_type="application/json".
Checklist: Before You Change WORKERS
- [ ] Are you running Purple8 as an embedded engine (default)? →
WORKERS=1 - [ ] Do you need more throughput? → Add a second container instance with its own volume; do not fork the same instance
- [ ] Are you running a pure stateless FastAPI service that does NOT use
GraphEngine? →WORKERS=Nis fine; setP8G_ALLOW_MULTI_WORKER=1to suppress the warning - [ ] Running Locust/k6 benchmarks? → Seed and query against the same worker (
WORKERS=1) or your latency numbers will be meaningless