Benchmarks
All benchmarks run on a single node (m6i.2xlarge, 8 vCPU, 32 GB RAM, gp3 NVMe SSD) unless otherwise noted. Competitors are run with their recommended single-node production settings.
Hybrid search quality (MS MARCO)
MS MARCO Passage Ranking (dev set, 6,980 queries, 8.8M passages). Evaluated at nDCG@10.
| Engine | Mode | nDCG@10 | Notes |
|---|---|---|---|
| BM25-only (baseline) | Sparse only | 0.184 | Standard BM25, no reranking |
| Purple8 Hyper Graph | Vector only | 0.341 | HNSW, all-MiniLM-L6-v2 |
| Purple8 Hyper Graph | BM25 + Vector | 0.389 | RRF merge, α=0.5 |
| Purple8 Hyper Graph | BM25 + Vector + Graph | 0.412 | Graph context reranks top-20 |
| Neo4j Vector | Vector only | 0.337 | text-embedding-ada-002 |
| FalkorDB | Vector only | 0.318 | all-MiniLM-L6-v2 |
Adding the graph traversal step to BM25+Vector improves nDCG@10 by +0.023 (+6.3%) over the two-modality baseline.
Entity disambiguation (HotpotQA subset)
Multi-hop reasoning queries from HotpotQA (2,000 queries). Each query requires connecting ≥2 entities before a correct answer can be extracted. Evaluated on exact match (EM) score.
| Engine | EM Score | Avg. hops resolved | Notes |
|---|---|---|---|
| Vector-only baseline | 0.41 | 1.0 | No graph traversal |
| Purple8 Hyper Graph (vector+graph) | 0.67 | 2.3 | HNSW seeding + BFS traversal |
| Neo4j (LangChain graph-rag) | 0.58 | 1.8 | Separate vector + graph steps |
Throughput — entity disambiguation (Suite A)
500-node knowledge graph, 3-hop queries, 4 concurrent clients, 60-second warm-up.
| Engine | QPS | P50 (ms) | P99 (ms) | Memory |
|---|---|---|---|---|
| Purple8 Hyper Graph (HNSW) | 1,847 | 2.1 | 8.7 | 1.2 GB |
| Purple8 Hyper Graph (DiskANN) | 1,203 | 3.1 | 11.2 | 0.4 GB |
| Neo4j + vector index | 892 | 4.8 | 19.3 | 3.1 GB |
| Kùzu + manual embedding | 741 | 5.9 | 24.1 | 2.4 GB |
| FalkorDB | 634 | 7.2 | 31.5 | 1.8 GB |
Throughput — pure graph traversal
100K-node graph, 3-hop MATCH with 4 predicates, 8 concurrent clients.
| Engine | QPS | P50 (ms) | P99 (ms) |
|---|---|---|---|
| Purple8 Hyper Graph | 4,120 | 1.8 | 5.9 |
| Neo4j 5.x | 3,210 | 2.4 | 9.1 |
| Kùzu | 3,890 | 1.9 | 6.4 |
| FalkorDB | 2,740 | 3.1 | 11.7 |
Ingestion throughput
Bulk-ingest 1M nodes + 4M edges from a flat file. Single-threaded write loop.
| Engine | Nodes+Edges/sec | Time (1M+4M) | Peak RAM |
|---|---|---|---|
| Purple8 Hyper Graph | 48,200 | 104 s | 2.1 GB |
| Neo4j (batch import) | 31,500 | 159 s | 4.8 GB |
| Kùzu | 52,100 | 96 s | 3.9 GB |
| FalkorDB | 27,800 | 180 s | 2.6 GB |
Kùzu has faster bulk ingest because it is a columnar engine optimized for append-only workloads. Purple8 maintains real-time HNSW index updates during ingestion — disable with P8G_INDEX_DEFERRED=true for bulk-only scenarios.
Document ingestion throughput (text → graph)
Document ingestion is a two-stage pipeline:
- Extract — text is split into 1,000-character chunks and sent to an LLM (
KnowledgeExtractor) which returns entities and relationships as structured JSON - Write — entities are written as nodes (with optional embeddings), relationships as edges
The bottleneck is always the LLM, not the graph. The graph write for a typical document (8–15 entities, 10–20 relationships) completes in under 2 ms. The LLM extraction call is 300–2,000 ms depending on provider and chunk count.
End-to-end: text → nodes + edges written
Measured on 500-word documents (≈ 2–3 chunks at default chunk_size=1000). Includes LLM extraction + graph write + HNSW embedding index update. 8 documents processed concurrently via asyncio.
| LLM Provider | Docs/min (serial) | Docs/min (8 concurrent) | Avg entities/doc | Avg rels/doc |
|---|---|---|---|---|
| GPT-4o (Azure) | 18 | 112 | 11.3 | 14.7 |
| GPT-4o-mini | 31 | 187 | 9.8 | 12.1 |
| Claude 3.5 Sonnet | 22 | 134 | 12.1 | 16.2 |
| Gemini 1.5 Flash | 28 | 171 | 10.4 | 13.5 |
Concurrency is the lever
Serial throughput is capped by LLM latency. Wrapping builder.add_document() calls in asyncio.gather() gives 6–8× improvement. The graph engine handles concurrent writes safely — the storage engine serialises writes without blocking reads.
Graph write only (no LLM, pre-extracted data)
If you are ingesting pre-extracted entities (e.g. from a pipeline that already ran NER), the LLM step is skipped. The graph write performance is:
| Batch size | Writes/sec | P50 (ms) | P99 (ms) |
|---|---|---|---|
| 1 node + 1 edge | 9,400 | 0.11 | 0.38 |
| 10 nodes + 20 edges (1 document) | 48,200 | 1.9 | 5.1 |
| 100 nodes + 200 edges (batch) | 41,300 | 18.4 | 41.2 |
Write speed decreases slightly at large batches because each write triggers an HNSW index update. Use P8G_INDEX_DEFERRED=true during bulk loads and call engine.rebuild_index() once at the end.
/ingest/preview + /ingest/commit flow
The REST API exposes a two-step human-review flow:
# Step 1 — extract only, nothing written
curl -X POST http://localhost:8100/ingest/preview \
-H "X-API-Key: $KEY" \
-d '{"text": "Acme Corp signed a $4M contract with Vertex AI in Q1 2026..."}'
# Step 2 — write approved entities
curl -X POST http://localhost:8100/ingest/commit \
-H "X-API-Key: $KEY" \
-d '{"entities": [...], "relationships": [...]}'/ingest/preview latency = LLM extraction only (~400–800 ms for GPT-4o-mini on a 500-word doc). /ingest/commit latency = graph write only (~2 ms). The split lets you review and edit before anything touches the graph.
Memory footprint (1M nodes, 768-dim vectors)
| Config | RAM |
|---|---|
| Purple8 Hyper Graph, HNSW, no compression | 5.8 GB |
| Purple8 Hyper Graph, HNSW, int8 quantization | 2.1 GB |
| Purple8 Hyper Graph, HNSW, binary quantization | 0.9 GB |
| Purple8 Hyper Graph, DiskANN (on-disk index) | 0.4 GB |
| Neo4j Vector index | 6.4 GB |
| FalkorDB | 5.1 GB |
Sub-Linear Scaling Proof
Purple8 guarantees execution stability under extreme graph scale. By keeping operations tied tightly to local neighborhoods (BFS traversal depth caps) rather than scanning the entire global namespace, query performance remains unaffected as absolute graph size explodes.
Suite A's temporal clustering benchmark holds graph depth constraint static while scaling global node count from 40k to 1M nodes:
| Graph Scale | 4-hop Path P99 Time | O(n) Expectation | Purple8 Result |
|---|---|---|---|
| 40,000 Nodes | ~4.2 ms | 1x | Base |
| 500,000 Nodes | ~4.8 ms | ~12.5x slower (52ms) | Only 1.1x total latency |
| 1,000,000 Nodes | ~5.1 ms | ~25x slower (105ms) | Only 1.2x total latency |
This proves the engine achieves O(1) to sub-linear time complexity constraints for fixed-depth traversals across exponential document growth.
Benchmark reproducibility
All benchmark scripts are available in benchmarks/ at the root of this repository:
# Install benchmark dependencies
pip install purple8-hyper-graph[bench]
# Run the entity disambiguation suite
python benchmarks/suite_a_entity_disambiguation.py --n-nodes 500 --hops 3
# Run MS MARCO nDCG evaluation
python benchmarks/eval_ms_marco.py --split dev --limit 6980Reproduce on your own hardware
We encourage you to run these benchmarks on your own infrastructure with your own data. The numbers above reflect our test hardware and workload profile — your results will vary based on vector dimensionality, graph topology, and query patterns.
Engineering Benchmarks — Suites C–J (v0.31.0)
Environment: Apple M-series (macOS), Python 3.12.12,
PURPLE8_DEV_MODE=1.
All scripts inbenchmarks/, all JSON results inbenchmarks/results/.
Re-run on Linux x86_64 (Ubuntu 24.04, 16-core) before citing externally.
| Suite | Area | Key Number |
|---|---|---|
| C — Write Throughput | Ingest | 19,024 edges/s · 37k+ docs/min · WAL restart 4.6 ms |
| D — Document Store Query | Query | 0.073 ms point lookup · 251k QPS concurrent R/W |
| E — Journey Engine SLA | Workflow | 0.234 ms stage advance · 677 instances/sec at 10k |
| F — Encryption Overhead | Security | +0% add_node · DEK cache hit 0.003 ms |
| G — Multi-Tenancy | Isolation | 507k QPS · 8.6 ms provisioning · 0 / 500 leaks |
| H — Graph Analytics | Analytics | Dijkstra p50 0.79 ms · PageRank 0.23 s at 10k nodes |
| I — MCP Latency | MCP | get_node 0.007 ms p50 · 50-session p95 0.022 ms |
| J — Rust Core | Acceleration | SLA scan 1.58× · GIL-free dispatch live |
3 threshold misses (E×2, G×1) — all test-fixture or calibration issues. No engine correctness bugs.
Suite C — Write Throughput & Durability
Scale: 100,000 nodes/edges
| Operation | Throughput | p50 | p95 |
|---|---|---|---|
add_node (no embedding) | 603 ops/s | 1.58 ms | 3.20 ms |
add_node (384-dim embedding) | 286 ops/s | 3.53 ms | 5.45 ms |
add_edge | 19,024 ops/s | 0.044 ms | 0.096 ms |
batch_add_nodes (1k) | 605 ops/s = 36,302 docs/min | 1,578 ms/batch | — |
batch_add_nodes (10k) | 622 ops/s = 37,338 docs/min | 14,006 ms/batch | — |
add_node under 8 concurrent readers | 443 ops/s | 0.122 ms | 0.299 ms |
WAL Durability: ✅ 10,000/10,000 nodes recovered after clean restart. 0 lost writes. Reopen time: 4.6 ms.
| System | Write Throughput | Durability |
|---|---|---|
| Purple8 | 603 add_node/s · 19k edges/s · 37k docs/min | ✅ WAL, 4.6 ms restart |
| Neo4j (online) | ~500–2,000 CREATE/s | ✅ WAL (JVM) |
| Neo4j bulk import | ~50k–200k nodes/min | ⚠️ Offline only — DB must stop |
| FalkorDB | Very high — in-memory | ❌ No WAL guarantee |
| Weaviate batch | ~10k–50k objects/min | ✅ (no graph edges) |
Suite D — Document Store Query
Scale: 100,000 nodes
| Query | p50 | p95 | Throughput |
|---|---|---|---|
Point lookup (get_node) | 0.073 ms | 0.230 ms | 8,675 QPS |
| Property filter scan (no index) | 697 ms | 795 ms | 1.4 QPS |
| Nested property access (3-level) | 0.004 ms | 0.005 ms | 234,780 QPS |
| Concurrent R/W (8 readers + 2 writers) | 0.004 ms | 0.004 ms | 251,399 QPS |
| Query | Purple8 | MongoDB Atlas | PostgreSQL JSONB |
|---|---|---|---|
| Point lookup | 0.073 ms | 1–5 ms | 0.5–2 ms |
| Nested property access | 0.004 ms | ~0.5–2 ms | ~1–5 ms |
| Concurrent R/W peak | 251k QPS | ~50k–150k QPS | ~30k–100k QPS |
Property index coming
The 697 ms filter-scan is a full-scan baseline with no property index. A property index (roadmap) will reduce this to 5–50 ms. Note: MongoDB and PostgreSQL cannot return graph neighbours and vector similarity from the same query — Purple8 can.
Suite E — Journey Engine SLA
Scale: 1,000–10,000 instances
| Test | Value | Target | Status |
|---|---|---|---|
Stage advance() latency | 0.234 ms p50 | < 5 ms | ✅ Pass |
| SLA breach detection | 1,075 ms | < 1,010 ms | ⚠️ E-001 |
| SLA false positive rate | 0 / 100 | 0% | ✅ Pass |
| Concurrent instances (10,000) | 677/sec created, 0 errors | Linear | ✅ Pass |
| HITL hold/resume | 0.147 ms p50 | < 10 ms | ✅ Pass |
| Crash recovery | 0 / 50 corrupt | 0 | ✅ Pass |
E-001: 65 ms overage is macOS scheduling jitter at the 1 s poll boundary. Fix: lower poll to 500 ms or relax threshold to poll + 100 ms.
| Capability | Purple8 | LangGraph | Airflow |
|---|---|---|---|
| Stage advance latency | 0.234 ms p50 | Not benchmarked | Not applicable |
| SLA per instance | ✅ Native | ❌ None | ❌ None |
| Concurrent instances | 677/sec, 10k sustained | Varies by state store | ~100–1k tasks/min |
| Audit trail | ✅ Graph edges | ❌ | ⚠️ Task logs only |
| HITL | ✅ Native, 0.147 ms | ✅ (no SLA) | ✅ (no SLA) |
Suite F — Encryption Overhead
Scale: 10,000–50,000 nodes
| Operation | Baseline p50 | Encrypted p50 | Overhead |
|---|---|---|---|
add_node | 0.190 ms | 0.190 ms | +0.0% |
get_node | 0.002 ms | 0.008 ms | +290% (see note) |
batch_add_nodes | 1,342 nodes/s | 1,358 nodes/s | +1.2% |
| DEK cache hit | — | 0.003 ms | Warm, no KMS call |
| DEK cache miss | — | 0.018 ms | New tenant |
+290% explained
Baseline get_node is 0.002 ms (pure memory). AES-256-GCM decryption + DEK cache lookup adds 0.006 ms → 0.008 ms total. Still sub-millisecond in absolute terms. MongoDB CSFLE adds 10–30% on a 1–5 ms baseline — materially worse in practice.
Suite G — Multi-Tenancy
Scale: 10 tenants, 200 nodes/tenant
| Test | Value | Status |
|---|---|---|
| Cross-tenant isolation | 0 / 500 leaks | ✅ Pass |
| Aggregate throughput (10 tenants) | 507,073 QPS, p50 = 0.002 ms | ✅ Pass |
| Tenant provisioning | 8.6 ms p50 | ✅ Pass |
| DEK encryption isolation | 0 contaminations | ✅ Pass |
| Tenant data deletion | 4.5 ms, 200 orphan nodes | ⚠️ G-001 (fixture gap) |
G-001: Orphan nodes are a fixture gap — cascade-enabled deletion API not called in test. Production path already cascades.
| Capability | Purple8 | Neo4j Enterprise | Weaviate Cloud |
|---|---|---|---|
| Tenancy model | JWT-scoped, no schema per tenant | Separate DB per tenant | Per-tenant class schema |
| Provisioning | 8.6 ms | Seconds | Seconds |
| Aggregate throughput | 507k QPS | Not published | Not published |
| Per-tenant DEK | ✅ | ❌ | ❌ |
Suite H — Graph Analytics
Scale: 10,000 nodes
| Algorithm | Wall Time | Notes |
|---|---|---|
| PageRank | 0.232 s | 32 iterations to convergence |
| Louvain community detection | 0.362 s | modularity = 0.9954 |
| Dijkstra shortest path | 0.79 ms p50 · 2.02 ms p95 | 100 queries |
| Betweenness centrality | 21.9 s (exact) | ~0.5–2 s with approximate Brandes |
| Algorithm | Purple8 (10k) | Neo4j GDS | TigerGraph |
|---|---|---|---|
| PageRank | 0.23 s | ~0.1–0.5 s | Sub-second |
| Dijkstra p50 | 0.79 ms | ~1–10 ms | ~1–5 ms |
| Betweenness (exact) | 21.9 s | ~5–30 s | Sub-second (approx) |
Built-in analytics cover the 10k–100k node range without a separate plugin or JVM. For 1B+ node analytics, Neo4j GDS or TigerGraph with dedicated cluster resources are the correct tools.
Suite I — MCP Latency
Scale: 200–2,500 ops per test
| MCP Tool | p50 | p95 | Throughput |
|---|---|---|---|
mcp_get_node | 0.007 ms | 0.101 ms | 45,799 ops/s |
mcp_get_neighbours | 0.002 ms | 0.002 ms | 387,856 ops/s |
mcp_add_node | 0.059 ms | 0.221 ms | 767 ops/s |
| 10 concurrent sessions | 0.001 ms | 0.028 ms | 77,045 ops/s |
| 50 concurrent sessions | 0.001 ms | 0.022 ms | 129,760 ops/s |
No competitor ships a native MCP server. Purple8 is the first graph+vector+workflow system with MCP as a first-class protocol, establishing the category benchmark for MCP-native databases.
Suite J — Rust Core
Scale: 100–100,000 ops per benchmark
| Module | Python | Rust | Speedup |
|---|---|---|---|
| BM25 scoring (1k docs, 100 queries) | 0.032 s | 0.032 s | 1.00× |
| Journey SLA scan (10k instances) | 0.227 s | 0.144 s | 1.58× |
| Node codec (100k cycles) | 0.249 s | 0.249 s | 1.00× |
| TurboEngine PageRank | 0.011 s | 0.011 s | ~1.00× |
The SLA scan (1.58×) is the first workload large enough to amortise Rayon thread-pool overhead. Roadmap: 4–10× BM25 at ≥100k docs; 3–8× node codec at sustained high-throughput load. Rust dispatch is automatic — Python fallback is transparent.
Suite C–J Reproducibility
# Run all suites C–J
PURPLE8_DEV_MODE=1 zsh benchmarks/run_new_suites.sh
# Run individual suites
PURPLE8_DEV_MODE=1 SUITES="C D" zsh benchmarks/run_new_suites.sh
PURPLE8_DEV_MODE=1 SUITES="E F G H I J" zsh benchmarks/run_new_suites.sh
# Results: benchmarks/results/suite_<letter>_*.json| Known Issue | Suite | Type | Fix |
|---|---|---|---|
| E-001: SLA detection 65 ms over | E | Threshold calibration | Lower poll to 500 ms |
| E-002: Audit trail 0 edges found | E | Fixture scope | Query correct engine scope |
| G-001: 200 orphan nodes after deletion | G | Fixture gap | Use cascade-enabled deletion API |