Performance & Monitoring¶
Startup speed¶
The system connects to PostgreSQL directly at server startup; the data is pre-loaded, so no ETL runs on each boot.
- First deployment: terminology data must first be imported from the admin console's module pages (Terminology etc.) — roughly 1 minute for ICD, 5–15 minutes for SNOMED CT.
- Subsequent starts: the service connects straight to PostgreSQL and starts within seconds.
- Initialisation model: the Node server creates the connection pool, Redis, and each service exactly once at process start (
main()innode-server/src/server.ts); each MCP session holds its own transport but shares these process-level singletons. (The old PythonmcpSDK's lifespan-per-session problem no longer exists.)
Recommendation: make sure the PostgreSQL data volume is persistent (compose.yaml sets this up by default) so the loaded terminology data survives.
Query performance¶
- pgBouncer connection pool: transaction mode, 500 client connections mapped onto 30 PG connections, for high concurrency.
- Redis cache (not in use today):
node-server/src/cache.tsprovidescached(ttl, prefix)— a Redis-backed, fail-open memoizer (on a cache error the function simply runs) — but no service or tool calls it yet, so every tool call hits PostgreSQL. Wrapping a hot read path incached()is the intended way to add caching. - FTS indexes: every major search column has a PostgreSQL Full-Text Search index.
- Vector search: the
*_embeddingstables use pgvector. ICD semantic search deliberately bypasses the HNSW index with an exact scan (ORDER BY (embedding <=> …) + 0): the ~50k near-duplicate 7th-character codes make HNSW recall collapse, and the exact scan over ~98k rows takes about 0.35 s. pgdriver: uses unnamed prepared statements to stay compatible with pgBouncer transaction mode.
Concurrency¶
The MCP server is implemented in Node.js (Express + @modelcontextprotocol/sdk). To handle heavy request volumes:
- Tune pgBouncer's
MAX_CLIENT_CONN(default 500) andDEFAULT_POOL_SIZE(default 30). - Watch pool usage through Prometheus (
mcp_db_pool_size/mcp_db_pool_checked_out). - Multiple container instances can share one PostgreSQL and one Redis.
Background jobs¶
ADMIN_MAX_CONCURRENT_JOBSbounds how many jobsadmin-workerruns at once (4 by default in compose); per-module resource slots additionally prevent parallel imports of the same module.- The worker's
NODE_OPTIONS=--max-old-space-size=8192is necessary: IG imports load whole dependency packages (such ashl7.fhir.r4.examples) into memory. Do not lower it. - On a memory-constrained host, avoid running several large imports at once (e.g. SNOMED CT and an IG); an idle worker may also retain gigabytes of heap, so restarting it before a large import can help.
Monitoring¶
The Prometheus metrics endpoint is /metrics on METRICS_PORT (9090 by default, bound to 127.0.0.1 only). node-server/src/metrics.ts defines:
| Metric | Type | Status |
|---|---|---|
mcp_db_pool_size / mcp_db_pool_checked_out |
Gauge | Updated periodically by the DB stats collector |
mcp_tool_requests_total{tool,status} / mcp_tool_duration_seconds{tool} |
Counter / Histogram | Recorded on every tool call (MCP, POST /tools/<name> and the admin playground share one dispatch point, withToolMetrics in mcp.ts). Labels are only the tool name and status (success / error; a throw or an isError result counts as error) — arguments are never recorded; calls rejected by argument validation before reaching the handler are not counted |
mcp_cache_operations_total{prefix,result} |
Counter | Increments only when cached() is used; it currently has no call site |