Performance Tuning Guide
Product: v0.19.0 · Contract: OpenAPI · Spec ops: Ingestion cancel & fairness
Performance Tuning Guide
Section titled “Performance Tuning Guide”Optimizing EdgeQuake for Production Workloads
Capacity / sizing SSOT: Product limits — pick host RAM, shared_buffers, and Wave-2 env from the sizing table before tuning LLM knobs.
Vector search (pgvector): For ~100k filtered ANN, use the Wave-2 greenfield recipe (halfvec + EDGEQUAKE_HNSW_PARTIAL_BY_WORKSPACE=1). SPEC-067 applies session-local planner bias (enable_seqscan=off, random_page_cost=1.1) when a workspace partial HNSW is ready and filters are column-only. Do not invent ad-hoc CREATE INDEX … ON embeddings SQL — EdgeQuake owns eq_*_vectors DDL.
Claim ladders (make ceiling-proof) are honesty gates, not day-2 sizing.
Performance Overview
Section titled “Performance Overview”┌─────────────────────────────────────────────────────────────────┐│ PERFORMANCE BOTTLENECKS │├─────────────────────────────────────────────────────────────────┤│ ││ Request Latency Breakdown (typical hybrid query): ││ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ Phase │ Time │ Bottleneck │ ││ ├──────────────────────────────────────────────────────────┤ ││ │ Embedding │ 50ms │ LLM API latency │ ││ │ Vector Search │ 20ms │ pgvector index │ ││ │ Graph Traverse │ 30ms │ Apache AGE queries │ ││ │ LLM Generation │ 2000ms │ Token generation (dominant) │ ││ │ Network/Parse │ 50ms │ Serialization │ ││ ├──────────────────────────────────────────────────────────┤ ││ │ TOTAL │ ~2150ms │ LLM is 93% of latency │ ││ └──────────────────────────────────────────────────────────┘ ││ ││ Key Insight: Optimizing LLM selection has largest impact ││ │└─────────────────────────────────────────────────────────────────┘Quick Wins
Section titled “Quick Wins”1. Choose the Right LLM for the Workload
Section titled “1. Choose the Right LLM for the Workload”Latency varies by provider, hardware, and context size — do not treat static TTFT tables as SSOT. Measure with your models and GET /api/v1/pipeline/queue-metrics.
| Workload | Starting point |
|---|---|
| Production cloud ingest/query | gpt-5-mini or gpt-4.1-nano (cost/latency balance) |
Local dev (make dev, no API key) |
ollama / gemma4:latest |
| Vision PDF convert (unset env) | ollama / gemma4:latest per vision_env.rs; cloud: set EDGEQUAKE_VISION_* explicitly |
Pin models via EDGEQUAKE_DEFAULT_LLM_MODEL (or Makefile / .env.example — see Configuration).
2. Reduce Context Size
Section titled “2. Reduce Context Size”Smaller context = faster LLM processing:
# Query with fewer chunkscurl -X POST http://localhost:8080/api/v1/query \ -d '{"query": "...", "max_chunks": 5, "max_entities": 5}'Default vs Optimized:
| Setting | Default | Optimized |
|---|---|---|
max_chunks |
20 | 5-10 |
max_entities |
10 | 3-5 |
max_relationships |
20 | 5-10 |
3. Use Appropriate Query Mode
Section titled “3. Use Appropriate Query Mode”| Mode | Speed | Use Case |
|---|---|---|
naive |
Fastest | Simple factual queries |
local |
Fast | Entity-focused queries |
hybrid |
Medium | General queries |
global |
Slow | Overview/theme queries |
# Fast mode for simple queriescurl -X POST http://localhost:8080/api/v1/query \ -d '{"query": "What is X?", "mode": "naive"}'Document Processing Optimization
Section titled “Document Processing Optimization”Worker Configuration
Section titled “Worker Configuration”# Default: Uses all CPU cores# For I/O bound workloads (LLM API calls), use 2x coresexport WORKER_THREADS=8 # For 4-core machine
# Fairness cap (default ≈ ¾ of WORKER_THREADS)# MAX_TASKS_PER_TENANT=0 # disable limiterTenant Fairness & Local LLM Clamp (SPEC-057)
Section titled “Tenant Fairness & Local LLM Clamp (SPEC-057)”When MAX_TASKS_PER_TENANT > 0, workers park excess tasks on a per-tenant semaphore — no 500ms requeue storm. Parked tasks release their DB claim before waiting; monitor tenant_park_waiters on queue-metrics.
Local providers (ollama, lmstudio) clamp to 1 concurrent task per tenant unless EDGEQUAKE_ALLOW_LOCAL_HIGH_CONCURRENCY=1. Hybrid mode: set EDGEQUAKE_EXTRACT_PROVIDER=ollama when LLM is cloud but extract runs locally so the clamp applies.
Task Lease & Multi-Replica (SPEC-057 P1/P3)
Section titled “Task Lease & Multi-Replica (SPEC-057 P1/P3)”| Variable | Tuning note |
|---|---|
EDGEQUAKE_TASK_LEASE_TTL_SECS |
Default 120; heartbeat every 60s |
EDGEQUAKE_STARTUP_AUTO_RESUME |
Default ON (unset); set 0 for Interrupted Failed + manual Reprocess |
EDGEQUAKE_REPLICAS + EDGEQUAKE_TASK_DELIVERY |
REPLICAS>1 requires bridged or notify_only |
Adaptive Timeouts — LargeDocumentProfile (SPEC-038 / SPEC-057 P2)
Section titled “Adaptive Timeouts — LargeDocumentProfile (SPEC-038 / SPEC-057 P2)”Convert and ingest run as separate tasks with independent timeouts derived from page count:
| Phase | Task type | Timeout source |
|---|---|---|
| Convert | pdf_processing |
LargeDocumentProfile::convert_timeout_secs (+ Pass B budget) |
| Ingest | insert |
LargeDocumentProfile::ingest_timeout_secs |
Override both phases with TASK_PROCESSING_TIMEOUT_SECS (legacy single knob). Floors/ceilings: 7200s–86400s. Upload ETA and admission routing use the same profile — see edgequake-api/src/services/large_document_profile.rs.
Chunk Size Tuning
Section titled “Chunk Size Tuning”┌─────────────────────────────────────────────────────────────────┐│ CHUNK SIZE TRADEOFFS├─────────────────────────────────────────────────────────────────┤││ Small chunks (256 tokens):│ ✅ More precise retrieval│ ✅ Lower token cost per extraction│ ❌ More LLM calls (slower processing)│ ❌ Less context per chunk││ Large chunks (1024 tokens):│ ✅ Fewer LLM calls (faster processing)│ ✅ Better context preservation│ ❌ Less precise retrieval│ ❌ Higher token cost per extraction││ Recommendation: 1200 tokens (default, balanced)│└─────────────────────────────────────────────────────────────────┘Batch Processing
Section titled “Batch Processing”For bulk uploads, process in batches:
# Upload via batch endpoint (more efficient)curl -X POST http://localhost:8080/api/v1/documents/upload/batch \ -F "files=@doc1.pdf" \ -F "files=@doc2.pdf" \ -F "files=@doc3.pdf"Graph UI Optimization
Section titled “Graph UI Optimization”The WebUI graph viewer uses Sigma.js and Graphology. For interactive graphs, browser-side lifecycle mistakes are often more expensive than backend latency.
Current defaults
Section titled “Current defaults”- Layout selection reuses a single shared layout engine.
- Large graph thresholds reduce label density and disable expensive edge events.
- Hover and selection emphasis are handled through Sigma reducers plus
scheduleRefresh()rather than broad graph mutations. - Streaming graph updates append nodes and edges incrementally instead of rebuilding the renderer.
Operational guidance
Section titled “Operational guidance”- Prefer
forcefor general exploration andcircularorhierarchicalwhen you want faster deterministic rearrangement. - Keep edge labels off for dense graphs unless relationship text is essential.
- If you extend the graph UI, add new layout logic only in
edgequake_webui/src/lib/graph/layouts.ts. - If you add new edge-identity rules, keep them centralized in
edgequake_webui/src/lib/graph/ids.ts.
Anti-patterns to avoid
Section titled “Anti-patterns to avoid”- Recreating the Sigma instance for a plain layout switch.
- Long-lived animation loops that refresh the full graph continuously.
- Re-implementing layout parameters in multiple components.
- Mutating every node and edge on hover when a reducer can express the same visual state.
Database Optimization
Section titled “Database Optimization”PostgreSQL Configuration
Section titled “PostgreSQL Configuration”postgresql.conf tuning for EdgeQuake:
# Memory (adjust for your RAM)shared_buffers = 4GB # 25% of RAMeffective_cache_size = 12GB # 75% of RAMwork_mem = 256MB # For complex queriesmaintenance_work_mem = 1GB # For indexing
# Connectionsmax_connections = 200 # Match app pool size
# Write Ahead Logwal_buffers = 64MBcheckpoint_completion_target = 0.9
# Query Planningrandom_page_cost = 1.1 # For SSD storageeffective_io_concurrency = 200 # For SSD storage
# Parallel Querymax_parallel_workers_per_gather = 4max_parallel_workers = 8Connection Pooling
Section titled “Connection Pooling”Use PgBouncer for high-concurrency:
[databases]edgequake = host=localhost port=5432 dbname=edgequake
[pgbouncer]pool_mode = transactionmax_client_conn = 1000default_pool_size = 50reserve_pool_size = 10Connection String:
# Via PgBouncer (port 6432)DATABASE_URL="postgresql://user:pass@localhost:6432/edgequake"pgvector Index Tuning
Section titled “pgvector Index Tuning”-- Check current index\d embeddings
-- Optimal HNSW parameters for performanceCREATE INDEX CONCURRENTLY embeddings_vector_idxON embeddingsUSING hnsw (embedding vector_cosine_ops)WITH (m = 16, ef_construction = 64);
-- For higher recall (slower)-- WITH (m = 32, ef_construction = 128);Search Quality vs Speed:
| ef_search | Recall | Latency |
|---|---|---|
| 40 | 95% | 10ms |
| 100 | 98% | 20ms |
| 200 | 99% | 40ms |
-- Set search quality at runtimeSET hnsw.ef_search = 100;Apache AGE Tuning
Section titled “Apache AGE Tuning”-- Ensure graph is loaded in memorySET search_path = ag_catalog, "$user", public;LOAD 'age';
-- Index commonly filtered propertiesSELECT create_vlabel('edgequake_graph', 'Entity');SELECT create_elabel('edgequake_graph', 'Relationship');Query Optimization
Section titled “Query Optimization”Embedding Caching
Section titled “Embedding Caching”EdgeQuake caches embeddings for repeated queries:
┌─────────────────────────────────────────────────────────────────┐│ QUERY CACHING │├─────────────────────────────────────────────────────────────────┤│ ││ Query "What is X?" ──→ [Embedding Cache] ──→ Vector Search ││ │ ││ Cache Hit: 0ms ││ Cache Miss: 50ms ││ ││ Cache is in-memory, cleared on restart ││ │└─────────────────────────────────────────────────────────────────┘Reranking Strategy
Section titled “Reranking Strategy”Reranking improves quality but adds latency:
# Disable reranking for faster queriescurl -X POST http://localhost:8080/api/v1/query \ -d '{"query": "...", "enable_rerank": false}'
# Or use smaller rerank setcurl -X POST http://localhost:8080/api/v1/query \ -d '{"query": "...", "rerank_top_k": 3}'| Reranking | Latency | Quality |
|---|---|---|
| Disabled | -100ms | Baseline |
| Top 3 | +30ms | +5% |
| Top 5 | +50ms | +8% |
| Top 10 | +100ms | +10% |
Query Prefetching
Section titled “Query Prefetching”For chat applications, prefetch likely follow-up queries:
// Client-side optimizationasync function queryWithPrefetch(query) { const response = await fetch("/api/v1/query", { method: "POST", body: JSON.stringify({ query }), });
// Prefetch entity expansions in background const entities = extractEntities(await response.json()); entities.slice(0, 3).forEach((entity) => { fetch(`/api/v1/graph/entities/${entity}/neighborhood`); });}LLM Provider Optimization
Section titled “LLM Provider Optimization”OpenAI Optimization
Section titled “OpenAI Optimization”# Use streaming for faster time-to-first-tokencurl -X POST http://localhost:8080/api/v1/query/stream \ -H "Accept: text/event-stream" \ -d '{"query": "..."}'Ollama Optimization
Section titled “Ollama Optimization”GPU Acceleration:
# Ensure CUDA is availablenvidia-smi
# Set GPU layers (more = faster, more VRAM)export OLLAMA_NUM_GPU=50ollama serveModel Quantization:
| Quantization | Speed | Quality | VRAM |
|---|---|---|---|
| Q4_K_M | Fastest | Good | 4GB |
| Q5_K_M | Fast | Better | 5GB |
| Q8_0 | Slow | Best | 8GB |
| FP16 | Slowest | Reference | 16GB |
# Download quantized modelollama pull gemma4:latest-q4_K_MLocal vs Cloud Latency
Section titled “Local vs Cloud Latency”Measure in your environment. Local Ollama on GPU often wins on time-to-first-token for short contexts; cloud models win on throughput and extraction quality at scale. Use queue-metrics pressure and document display_status to spot fairness stalls vs true LLM slowness.
Scaling Strategies
Section titled “Scaling Strategies”Horizontal Scaling
Section titled “Horizontal Scaling”┌─────────────────────────────────────────────────────────────────┐│ HORIZONTAL ARCHITECTURE │├─────────────────────────────────────────────────────────────────┤│ ││ Load Balancer ││ │ ││ ┌─────────────────┼─────────────────┐ ││ ↓ ↓ ↓ ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ EdgeQuake 1 │ │ EdgeQuake 2 │ │ EdgeQuake 3 │ ││ │ (Queries) │ │ (Queries) │ │ (Processing)│ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ ││ │ │ │ ││ └─────────────────┼─────────────────┘ ││ ↓ ││ ┌─────────────┐ ││ │ PostgreSQL │ ││ │ + Replicas │ ││ └─────────────┘ ││ │└─────────────────────────────────────────────────────────────────┘Kubernetes HPA:
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: edgequake-hpaspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: edgequake minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70Read Replicas
Section titled “Read Replicas”Separate read and write workloads:
# Primary for writesDATABASE_URL="postgresql://user:pass@primary:5432/edgequake"
# Replica for reads (queries)DATABASE_READ_URL="postgresql://user:pass@replica:5432/edgequake"Monitoring Performance
Section titled “Monitoring Performance”Key Metrics
Section titled “Key Metrics”| Metric | Target | Alert |
|---|---|---|
| p50 query latency | <2s | >5s |
| p99 query latency | <10s | >30s |
| Processing throughput | >1 doc/min | <0.5 doc/min |
| Error rate | <1% | >5% |
| DB connection pool | <80% | >90% |
Prometheus Queries
Section titled “Prometheus Queries”# Query latency percentileshistogram_quantile(0.99, rate(edgequake_query_duration_seconds_bucket[5m]))
# Processing throughputrate(edgequake_documents_processed_total[5m])
# Error raterate(edgequake_query_errors_total[5m]) / rate(edgequake_query_total[5m])Benchmarking
Section titled “Benchmarking”# Run built-in benchmarkscargo bench
# Results:# vector_search 10.2 ms/iter# graph_traverse 5.1 ms/iter# entity_extraction 150 ms/iter (mock LLM)Performance Checklist
Section titled “Performance Checklist”Pre-Optimization
Section titled “Pre-Optimization”- Baseline metrics recorded
- Bottleneck identified (usually LLM)
- Resource monitoring in place
Quick Wins
Section titled “Quick Wins”- Model/provider chosen for workload (measure, don’t guess from static tables)
- Context size reduced (max_chunks ≤ 10)
- Appropriate query mode selected
- Streaming enabled for chat
-
tenant_park_waitersunderstood under local LLM clamp
Database
Section titled “Database”- PostgreSQL tuned for RAM
- pgvector HNSW index created
- Connection pooling enabled
- Read replicas for high load
Scaling
Section titled “Scaling”- Horizontal scaling configured
- Auto-scaling rules defined
- Load testing completed
- Graceful degradation planned
Troubleshooting Slow Queries
Section titled “Troubleshooting Slow Queries”Debug Query Timing
Section titled “Debug Query Timing”# Add timing to responsecurl -X POST http://localhost:8080/api/v1/query \ -d '{"query": "...", "debug": true}'Response:
{ "answer": "...", "stats": { "embedding_time_ms": 45, "retrieval_time_ms": 123, "generation_time_ms": 2890, "total_time_ms": 3058 }}Common Causes
Section titled “Common Causes”| Symptom | Cause | Fix |
|---|---|---|
| Slow embedding | Cold start | Warm up with test query |
| Slow retrieval | Missing index | Create HNSW index |
| Slow generation | Large context | Reduce max_chunks |
| Slow generation | Slow model | Switch to faster model |
| High latency variance | Connection pool | Enable PgBouncer |
Ingestion Pipeline Tuning (fixes #194)
Section titled “Ingestion Pipeline Tuning (fixes #194)”When ingesting large documents or using a slow local LLM (Ollama on a single GPU, LM Studio on CPU), the default pipeline limits can cause “Timeout after 180s” failures. Use these env vars to tune the ingestion pipeline:
Key variables
Section titled “Key variables”| Variable | Default | Guidance |
|---|---|---|
EDGEQUAKE_CHUNK_TIMEOUT_SECS |
180 |
Increase to match your LLM’s expected latency |
EDGEQUAKE_MAX_CONCURRENT_EXTRACTIONS |
16 |
Lower on a single GPU (use 2–4 for Ollama CPU) |
EDGEQUAKE_CHUNK_MAX_RETRIES |
3 |
Reduce to 1 for fast-fail during debugging |
EDGEQUAKE_CHUNK_RETRY_DELAY_MS |
1000 |
Increase to 5000 if the LLM needs warm-up time |
EDGEQUAKE_LLM_TIMEOUT_SECS |
600 |
Must be ≥ EDGEQUAKE_CHUNK_TIMEOUT_SECS |
Profiles
Section titled “Profiles”GPU server (powerful) — maximize throughput:
export EDGEQUAKE_CHUNK_TIMEOUT_SECS=120export EDGEQUAKE_MAX_CONCURRENT_EXTRACTIONS=32export EDGEQUAKE_LLM_TIMEOUT_SECS=600Single-GPU workstation — balanced:
export EDGEQUAKE_CHUNK_TIMEOUT_SECS=300export EDGEQUAKE_MAX_CONCURRENT_EXTRACTIONS=4export EDGEQUAKE_LLM_TIMEOUT_SECS=1800CPU-only Ollama — conservative:
export EDGEQUAKE_CHUNK_TIMEOUT_SECS=600export EDGEQUAKE_MAX_CONCURRENT_EXTRACTIONS=2export EDGEQUAKE_CHUNK_RETRY_DELAY_MS=5000export EDGEQUAKE_LLM_TIMEOUT_SECS=3600Cloud LLM (OpenAI / Anthropic) — fast, rate-limited:
export EDGEQUAKE_CHUNK_TIMEOUT_SECS=60export EDGEQUAKE_MAX_CONCURRENT_EXTRACTIONS=8 # stay under RPM limitsexport EDGEQUAKE_LLM_TIMEOUT_SECS=120Rule of thumb: Set
EDGEQUAKE_CHUNK_TIMEOUT_SECS= (time one LLM call takes for your biggest chunk) × 1.5 as a safety margin. Then setEDGEQUAKE_LLM_TIMEOUT_SECS≥ that value.
See Also
Section titled “See Also”- Configuration Reference - All settings
- Deployment Guide - Production setup
- Monitoring Guide - Observability