Performance Tuning
OctoFHIR includes built-in performance optimizations including query caching, automatic index analysis, and configurable tuning options.
Query Caching
Section titled “Query Caching”Overview
Section titled “Overview”OctoFHIR caches query execution plans to improve performance for repeated searches:
- Cache key: Based on parameter structure (not values)
- Cache hit: Skip query planning, reuse prepared statement
- Expected improvement: 30-50% faster for cached queries
Configuration
Section titled “Configuration”[search]default_count = 10max_count = 1000
# Query cache settingscache_enabled = truecache_capacity = 1000 # Maximum cached queriescache_max_age_secs = 3600 # Cache entry TTLHow It Works
Section titled “How It Works”- First request: Query is parsed, planned, and cached
- Subsequent requests: Cache hit, only values are bound
- Cache eviction: LRU (Least Recently Used) when capacity reached
Cache Statistics
Section titled “Cache Statistics”Monitor cache performance via admin endpoint:
GET /admin/cache/statsResponse:
{ "query_cache": { "hits": 15420, "misses": 1230, "size": 456, "capacity": 1000, "hit_ratio": 0.926 }}Cache Invalidation
Section titled “Cache Invalidation”The cache automatically invalidates when:
- TTL expires
- Server restarts
- Manual clear via admin API
POST /admin/cache/clearIndex Optimization
Section titled “Index Optimization”Automatic Index Analysis
Section titled “Automatic Index Analysis”OctoFHIR analyzes query performance and suggests indexes:
GET /admin/indexes/suggestions?resource_type=PatientResponse:
{ "suggestions": [ { "table": "patient", "column": "resource->>'family'", "type": "btree", "reason": "Frequently searched, currently using sequential scan", "estimated_improvement": "10x", "create_statement": "CREATE INDEX idx_patient_family ON patient ((resource->>'family'));" } ]}Recommended Indexes
Section titled “Recommended Indexes”For optimal performance, create these indexes:
Core Indexes (All Resources)
Section titled “Core Indexes (All Resources)”-- Resource type filtering (if using single table)CREATE INDEX idx_resource_type ON fhir_resources ((resource->>'resourceType'));
-- Last updated (for _lastUpdated searches and history)CREATE INDEX idx_resource_ts ON fhir_resources (ts DESC);
-- Resource ID lookupCREATE INDEX idx_resource_id ON fhir_resources ((resource->>'id'));
-- Version trackingCREATE INDEX idx_resource_txid ON fhir_resources (txid);Patient Indexes
Section titled “Patient Indexes”-- Name searchesCREATE INDEX idx_patient_family ON patient USING btree ( (resource->'name'->0->>'family'));
-- Identifier searchesCREATE INDEX idx_patient_identifier ON patient USING gin ( (resource->'identifier'));
-- Birth date range searchesCREATE INDEX idx_patient_birthdate ON patient USING btree ( (resource->>'birthDate'));Observation Indexes
Section titled “Observation Indexes”-- Code searches (most common)CREATE INDEX idx_observation_code ON observation USING gin ( (resource->'code'->'coding'));
-- Subject referenceCREATE INDEX idx_observation_subject ON observation USING btree ( (resource->'subject'->>'reference'));
-- Effective date rangesCREATE INDEX idx_observation_effective ON observation USING btree ( (resource->>'effectiveDateTime'));
-- Composite: code + date (common query pattern)CREATE INDEX idx_observation_code_date ON observation USING btree ( (resource->'code'->'coding'->0->>'code'), (resource->>'effectiveDateTime'));GIN vs BTREE
Section titled “GIN vs BTREE”| Index Type | Use Case | Example |
|---|---|---|
| BTREE | Equality, ranges | birthDate, _lastUpdated |
| GIN | JSONB containment, arrays | identifier, code.coding |
Analyzing Query Performance
Section titled “Analyzing Query Performance”Enable slow query logging:
[search]slow_query_threshold_ms = 100 # Log queries slower than 100msView slow queries:
GET /admin/queries/slow?limit=10Response:
{ "slow_queries": [ { "query_hash": "abc123", "execution_time_ms": 450, "resource_type": "Observation", "parameters": ["code", "date", "subject"], "sequential_scans": ["observation"], "suggestion": "Add index on (resource->'code'->'coding')" } ]}Connection Pooling
Section titled “Connection Pooling”Configuration
Section titled “Configuration”[storage.postgres]url = "postgres://user:pass@localhost/octofhir"pool_size = 20connect_timeout_ms = 5000idle_timeout_ms = 600000Sizing Guidelines
Section titled “Sizing Guidelines”| Concurrent Users | Pool Size |
|---|---|
| < 50 | 10-20 |
| 50-200 | 20-50 |
| 200-1000 | 50-100 |
| > 1000 | Use PgBouncer |
Monitoring
Section titled “Monitoring”GET /admin/db/stats{ "pool": { "size": 20, "available": 15, "in_use": 5, "waiting": 0 }, "queries": { "total": 125000, "avg_time_ms": 12.5 }}Redis Caching
Section titled “Redis Caching”Configuration
Section titled “Configuration”[redis]enabled = trueurl = "redis://localhost:6379"pool_size = 10timeout_ms = 1000Cache Layers
Section titled “Cache Layers”- Local cache (in-memory DashMap): Fastest, per-instance
- Redis cache: Shared across instances
- Database: Source of truth
Cache Invalidation
Section titled “Cache Invalidation”When resources are modified:
- Local cache entry removed
- Redis
PUBLISHsent to invalidation channel - Other instances receive notification and clear local cache
Search Optimization
Section titled “Search Optimization”Use Specific Parameters
Section titled “Use Specific Parameters”# Slower: scans all observationsGET /Observation?status=final
# Faster: filters by code first (likely indexed)GET /Observation?code=http://loinc.org|8867-4&status=finalLimit Results
Section titled “Limit Results”# Returns up to 10 results (default)GET /Patient?family=Smith
# Explicitly limitGET /Patient?family=Smith&_count=50
# Just get the countGET /Patient?family=Smith&_summary=countUse _elements
Section titled “Use _elements”# Returns full resourcesGET /Patient?family=Smith
# Returns only needed fields (less data transfer)GET /Patient?family=Smith&_elements=id,name,birthDateAvoid Large _include
Section titled “Avoid Large _include”# Can return many resourcesGET /Patient?_revinclude=Observation:subject
# Better: paginate and fetch separatelyGET /Patient?family=Smith&_count=10GET /Observation?subject=Patient/123&_count=50Monitoring and Metrics
Section titled “Monitoring and Metrics”Prometheus Metrics
Section titled “Prometheus Metrics”OctoFHIR exposes Prometheus metrics at /metrics:
# Query cacheoctofhir_query_cache_hits_totaloctofhir_query_cache_misses_totaloctofhir_query_cache_size
# Search performanceoctofhir_search_duration_seconds{resource_type, modifier}octofhir_search_results_total{resource_type}
# Transaction performanceoctofhir_transaction_duration_secondsoctofhir_transaction_entries_totaloctofhir_transaction_rollback_total
# Databaseoctofhir_db_pool_connections_totaloctofhir_db_query_duration_secondsoctofhir_slow_query_totalOpenTelemetry
Section titled “OpenTelemetry”Enable distributed tracing:
[otel]enabled = trueendpoint = "http://jaeger:4317"service_name = "octofhir"Spans include:
http.request- Full request lifecyclesearch.execute- Search executionsearch.cache_lookup- Cache operationsterminology.expand- ValueSet expansiontransaction.execute- Transaction processing
Benchmarking
Section titled “Benchmarking”Load Testing with k6
Section titled “Load Testing with k6”import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { stages: [ { duration: '30s', target: 50 }, { duration: '1m', target: 100 }, { duration: '30s', target: 0 }, ],};
export default function () { const res = http.get('http://localhost:8080/Patient?family=Smith', { headers: { 'Accept': 'application/fhir+json' }, });
check(res, { 'status is 200': (r) => r.status === 200, 'response time < 100ms': (r) => r.timings.duration < 100, });
sleep(0.1);}Run:
k6 run loadtest.jsExpected Performance
Section titled “Expected Performance”| Operation | Target | Notes |
|---|---|---|
| Simple search | < 50ms | Indexed parameter |
| Complex search | < 200ms | Multiple parameters |
| _include search | < 500ms | Depends on included count |
| Transaction (10 entries) | < 100ms | |
| ValueSet expansion | < 100ms | Cached |
| ValueSet expansion | < 2s | Uncached, large |
Troubleshooting
Section titled “Troubleshooting”Slow Queries
Section titled “Slow Queries”-
Check for missing indexes:
Terminal window GET /admin/indexes/suggestions -
Enable slow query logging:
[search]slow_query_threshold_ms = 50 -
Analyze specific query:
Terminal window POST /admin/queries/analyze{"query": "Patient?family=Smith&birthdate=lt1980"}
High Memory Usage
Section titled “High Memory Usage”-
Reduce cache sizes:
[search]cache_capacity = 500 -
Check for large result sets:
Terminal window GET /admin/queries/large_results
Connection Pool Exhaustion
Section titled “Connection Pool Exhaustion”-
Increase pool size:
[storage.postgres]pool_size = 50 -
Check for long-running queries:
Terminal window GET /admin/db/active_queries -
Consider using PgBouncer for connection pooling