Performance

June 1, 2026 · View on GitHub

Hades runs as a single JVM process — no database server, no orchestrator, no external services. On a laptop it sustains tens of thousands of FHIR terminology operations per second with sub-millisecond median latency on most operations. The numbers below were measured on 2026-05-31 against a 2.0.255 development build.

Headline figures

What it doesFHIR endpointMedian latency99th percentileThroughput
Single concept lookup (SNOMED)CodeSystem/$lookup316 µs6.36 ms25,800 req/s
Single concept lookup (LOINC)CodeSystem/$lookup527 µs1.48 ms17,000 req/s
Free-text search, 10 resultsValueSet/$expand757 µs2.65 ms11,500 req/s
FHIR search by canonical URLGET /ValueSet606 µs5.05 ms12,300 req/s
FHIR catalogue browse, 10 resultsGET /ValueSet3.37 ms4.42 ms2,900 req/s
Subsumption test (two SNOMED codes)CodeSystem/$subsumes221 µs636 µs38,900 req/s
Code validation against a value setValueSet/$validate-code241 µs1.20 ms34,900 req/s
Value set expansion (is-a hierarchy, 10 results)ValueSet/$expand225 µs771 µs37,400 req/s
Value set expansion (ECL refinement, 10 results)ValueSet/$expand1.16 ms3.39 ms7,500 req/s
Value set expansion (extensional, VSAC)ValueSet/$expand1.28 ms28.9 ms4,500 req/s
Concept translation (SNOMED → ICD-10)ConceptMap/$translate336 µs1.23 ms25,100 req/s

How to read the table

  • Median latency is the time the typical request takes — half of requests are faster, half are slower.
  • 99th percentile is the worst-case for 99 out of every 100 requests. It tells you the experience under normal load when something occasionally takes longer than usual.
  • Throughput is the sustained request rate, end-to-end over HTTP, with 10 concurrent clients hitting the server. Throughput scales with concurrency and core count up to the limits of the underlying data structures.

Why it's this fast

Hades resolves every operation against in-process data — there is no database round-trip on the hot path. SNOMED is served from an embedded LMDB + Lucene store via Hermes. LOINC and FHIR packages are served either from heap (plain Clojure hashmaps, ~nanosecond lookups) or from an on-disk SQLite container (JDBC + prepared statements, tens of microseconds). The same JVM process serves all terminologies.

Methodology

  • Hardware: Apple M1 Pro, 16 GB RAM
  • Hades: 2.0.255 development build, measured 2026-05-31
  • Data: SNOMED CT International 2025-02-01, LOINC 2.82, and the FHIR packages from fhir-tx.db (including the VSAC value sets)
  • Driver: wrk -t2 -c10 -d30s (2 threads, 10 keep-alive connections, 30 s of measurement) after a 10 s warmup at the same shape. Figures are the best of three runs — on a shared laptop, background load perturbs individual runs, so the least-contended run best reflects sustained capability
  • Variation: each operation rotates its requests through a pool so the server can't return a single cached response. $lookup, $subsumes and $validate-code rotate through 2002 SNOMED or LOINC codes; free-text search rotates through ~40 representative 3–6 character clinical prefixes (e.g. diab, card, pneum); the is-a and extensional $expand rows rotate through concepts and VSAC value sets respectively; $translate rotates SNOMED codes through the SNOMED → ICD-10 implicit map; the ECL $expand row uses a fixed refinement value set

Reproducing these figures

Start Hades serving the data above (e.g. clj -M:run serve snomed-intl-20250201.db loinc-2.82.db fhir-tx.db --port 8080), then run the self-contained script below — it needs wrk and jq, builds the request pools from the running server, writes the rotate script to a temp dir, and prints median / p99 latency and requests/sec for each operation.

BASE=http://localhost:8080
P="$(mktemp -d)"; trap 'rm -rf "$P"' EXIT
enc() { jq -rn --arg s "\$1" '$s|@uri'; }

# Request pools, built from the running server
curl -fsS "$BASE/fhir/ValueSet/\$expand?url=$(enc 'http://snomed.info/sct?fhir_vs=isa/404684003')&count=2002" \
  | jq -r '.expansion.contains[].code' > "$P/snomed.txt"
curl -fsS "$BASE/fhir/ValueSet/\$expand?url=$(enc 'http://loinc.org/vs')&count=2002" \
  | jq -r '.expansion.contains[].code' > "$P/loinc.txt"
curl -fsS "$BASE/fhir/ValueSet?_count=60" \
  | jq -r '.entry[].resource.url | select(test("cts.nlm"))' > "$P/vsac.txt"
printf '%s\n' diab card pneum hyper hypo asth angin arrhy brady tachy ische infarc \
  sepsis anaem leuk lymph melan carcin aden fract contus lacer abras burn ulcer gastr \
  hepat nephr cysti derm arthr myalg neuro psych depres anxiet schizo demen epilep migrai > "$P/prefixes.txt"
sed 's#^#http://snomed.info/sct?fhir_vs=isa/#' "$P/snomed.txt" > "$P/isa.txt"
echo 'http://snomed.info/sct?fhir_vs=ecl/<404684003:363698007=<<39057004' > "$P/ecl.txt"
printf '%s\n' 10 > "$P/counts.txt"

# Per request, substitute a pool value into the template at @@ (optionally %-encoded)
cat > "$P/rotate.lua" <<'LUA'
local pool = {}
for line in io.lines(os.getenv("WRK_POOL")) do
  if #line > 0 then pool[#pool + 1] = line end
end
local tmpl  = os.getenv("WRK_TEMPLATE")
local doenc = os.getenv("WRK_ENCODE") == "1"
local i = 0
local function urlencode(s)
  return (s:gsub("[^%w%-_%.~]", function(c) return string.format("%%%02X", string.byte(c)) end))
end
request = function()
  i = i + 1
  local v = pool[(i % #pool) + 1]
  if doenc then v = urlencode(v) end
  return wrk.format("GET", (tmpl:gsub("@@", function() return v end)))
end
LUA

# operation | pool | encode? | request path (@@ = rotated pool value)
ops="$(cat <<'EOF'
Single concept lookup (SNOMED)|snomed.txt|0|/fhir/CodeSystem/$lookup?system=http://snomed.info/sct&code=@@
Single concept lookup (LOINC)|loinc.txt|0|/fhir/CodeSystem/$lookup?system=http://loinc.org&code=@@
Free-text search, 10 results|prefixes.txt|0|/fhir/ValueSet/$expand?url=http%3A%2F%2Fsnomed.info%2Fsct%3Ffhir_vs&filter=@@&count=10
FHIR search by canonical URL|vsac.txt|1|/fhir/ValueSet?url=@@
FHIR catalogue browse, 10 results|counts.txt|0|/fhir/ValueSet?_count=@@&_summary=true
Subsumption test (two SNOMED codes)|snomed.txt|0|/fhir/CodeSystem/$subsumes?system=http://snomed.info/sct&codeA=@@&codeB=64572001
Code validation against a value set|snomed.txt|0|/fhir/ValueSet/$validate-code?url=http%3A%2F%2Fsnomed.info%2Fsct%3Ffhir_vs&system=http://snomed.info/sct&code=@@
Value set expansion (is-a hierarchy)|isa.txt|1|/fhir/ValueSet/$expand?url=@@&count=10
Value set expansion (ECL refinement)|ecl.txt|1|/fhir/ValueSet/$expand?url=@@&count=10
Value set expansion (extensional, VSAC)|vsac.txt|1|/fhir/ValueSet/$expand?url=@@
Concept translation (SNOMED → ICD-10)|snomed.txt|0|/fhir/ConceptMap/$translate?url=http%3A%2F%2Fsnomed.info%2Fsct%3Ffhir_cm%3D447562003&system=http://snomed.info/sct&code=@@
EOF
)"

printf '%-40s %10s %10s %12s\n' operation p50 p99 req/s
while IFS='|' read -r name pool enc tmpl; do
  [ -z "$name" ] && continue
  export WRK_POOL="$P/$pool" WRK_TEMPLATE="$tmpl" WRK_ENCODE="$enc"
  wrk -t2 -c10 -d10s          -s "$P/rotate.lua" "$BASE" >/dev/null 2>&1 || true   # warmup
  out="$(wrk -t2 -c10 -d30s --latency -s "$P/rotate.lua" "$BASE")"
  printf '%-40s %10s %10s %12s\n' "$name" \
    "$(awk '/^[[:space:]]*50%/{print \$2}'   <<<"$out")" \
    "$(awk '/^[[:space:]]*99%/{print \$2}'   <<<"$out")" \
    "$(awk '/Requests\/sec/{print \$2}'      <<<"$out")"
done <<< "$ops"

A registry-layer Criterium micro-benchmark suite (HTTP bypassed, useful for bisecting performance changes) is also available via clj -M:bench. I use this to check for performance regressions.

Comparing against other servers

Independent comparative benchmarks are welcome. Health Samurai's tx-benchmark runs a standard k6 load suite against FHIR terminology servers. For running it against Hades and comparing a fresh run with the published results, see doc/tx-benchmark.md.