Performance
This page summarises the results of performance testing conducted on openFHIR Enterprise. Since version 3.0.0, all performance measurements run on a dedicated, Terraform-provisioned Hetzner Cloud fleet: openFHIR alone on a server sized to each scenario, the database on its own server, and Apache JMeter driving load from a third server over a private network — a topology representative of real customer deployments.
All tests run under sustained concurrent load using Apache JMeter with 20 threads, a 30-second ramp-up, and 100 iterations per operation type (toFHIR and toOpenEHR) — 20 000 requests per scenario. The test dataset covers five mapping domains from the MII KDS profile set: Laborbericht, Fall, Procedure, Medikationsverabreichung, and a mixed set — 95 distinct request payloads per direction.
Note
Detailed breakdown of performance test available at https://open-fhir.com/performance-results
Test environment
One server per tier (Hetzner dedicated-vCPU CCX line, location fsn1), connected over a private network:
There is no docker CPU or memory pinning: each application server is itself
the resource limit, so JVM ergonomics and GC sizing see the real machine. The
memory axis is explored via JVM heap sizes per box, capped at ~75 % of box RAM
so the OS and off-heap memory keep headroom. The default matrix is 7
configurations (2|2g 2|4g 4|8g 4|12g 8|8g 8|16g 8|24g) × {postgres,
mongo} = 14 scenarios.
Two properties of the environment to keep in mind when reading the numbers:
Latencies include the private-network round trip (~0.2–0.5 ms) between the load generator and the app, and between the app and the database — as they do in a real deployment.
Dedicated vCPUs are still SMT threads on a shared physical host — good, though not laboratory-grade, isolation.
Test matrix
Measured with openfhir/openfhir-enterprise (version corresponds to version tied to this documentation).
Fourteen scenarios, varying vCPU count, JVM heap, and database
backend; every scenario completed all 20 000 requests with an error rate of
≤ 0.1 %.
vCPU |
Heap |
DB |
Mean (ms) |
Median (ms) |
P90 (ms) |
P95 (ms) |
|---|---|---|---|---|---|---|
2 |
2 GB |
MongoDB |
1 605 |
1 167 |
3 231 |
4 342 |
2 |
2 GB |
PostgreSQL |
1 828 |
1 322 |
3 667 |
4 928 |
2 |
4 GB |
MongoDB |
1 545 |
1 121 |
3 084 |
4 126 |
2 |
4 GB |
PostgreSQL |
1 781 |
1 299 |
3 570 |
4 794 |
4 |
8 GB |
MongoDB |
321 |
75 |
925 |
1 358 |
4 |
8 GB |
PostgreSQL |
478 |
169 |
1 264 |
1 922 |
4 |
12 GB |
MongoDB |
316 |
78 |
923 |
1 312 |
4 |
12 GB |
PostgreSQL |
500 |
173 |
1 331 |
2 043 |
8 |
8 GB |
MongoDB |
23 |
13 |
39 |
52 |
8 |
8 GB |
PostgreSQL |
51 |
22 |
77 |
146 |
8 |
16 GB |
MongoDB |
23 |
13 |
39 |
54 |
8 |
16 GB |
PostgreSQL |
52 |
22 |
79 |
163 |
8 |
24 GB |
MongoDB |
25 |
13 |
42 |
58 |
8 |
24 GB |
PostgreSQL |
44 |
22 |
69 |
123 |
Throughput and peak load
Sustained throughput and median latency observed during each run:
vCPU |
Heap |
DB |
Throughput (req/s) |
Median (ms) |
|---|---|---|---|---|
2 |
2 GB |
MongoDB |
84.3 |
1 167 |
2 |
2 GB |
PostgreSQL |
77.8 |
1 322 |
2 |
4 GB |
MongoDB |
87.5 |
1 121 |
2 |
4 GB |
PostgreSQL |
78.5 |
1 299 |
4 |
8 GB |
MongoDB |
168.2 |
75 |
4 |
8 GB |
PostgreSQL |
164.9 |
169 |
4 |
12 GB |
MongoDB |
170.0 |
78 |
4 |
12 GB |
PostgreSQL |
160.6 |
173 |
8 |
8 GB |
MongoDB |
199.1 |
13 |
8 |
8 GB |
PostgreSQL |
193.6 |
22 |
8 |
16 GB |
MongoDB |
193.7 |
13 |
8 |
16 GB |
PostgreSQL |
183.8 |
22 |
8 |
24 GB |
MongoDB |
185.2 |
13 |
8 |
24 GB |
PostgreSQL |
185.2 |
22 |
The highest sustained throughput observed was 199 req/s (8 vCPU / 8 GB heap / MongoDB). Under the tested 20-thread load profile the engine saturates around 165–170 req/s at 4 vCPU and 185–199 req/s at 8 vCPU, while latency keeps improving with core count — additional CPUs beyond 4 are better justified by latency SLO requirements than by raw throughput.
Analysis
CPU is the dominant factor. The step from 2 to 4 vCPU cuts mean latency ~5× and doubles throughput; 4 to 8 vCPU cuts mean latency a further ~10–14× (321 ms → 23 ms with MongoDB) and collapses the tail — P95 drops from 1 358 ms to 52 ms. Mapping is a CPU-bound workload; adding cores scales both latency and throughput up to the tested range.
Heap size has no measurable effect. At every box size, larger heaps perform within a few percent of the smallest one (8 GB vs 24 GB heap at 8 vCPU: identical 13 ms median; throughput differences within run-to-run noise). The engine does not benefit from heap beyond the baseline sizes — provision RAM for the OS and off-heap headroom, and spend the budget on vCPUs instead.
MongoDB leads PostgreSQL at every size. Median latency is 13 ms vs 22 ms at 8 vCPU and 75–78 ms vs 169–173 ms at 4 vCPU; throughput converges at 8 vCPU. The gap is most pronounced in the mid-tier, making MongoDB the better choice when running at 4 vCPU.
toFHIR and toOpenEHR behave comparably under sufficient resources. Both directions converge from 4 vCPU upward, indicating the engine itself is not the bottleneck at that resource level.
Recommendations
Minimum production configuration: 4 vCPU / 8 GB RAM, MongoDB — 168 req/s sustained throughput with a 75 ms median and 1.4 s P95.
Latency-sensitive deployments: 8 vCPU — 13 ms median / 52 ms P95 (MongoDB) or 22 ms median / 146 ms P95 (PostgreSQL).
Do not oversize the JVM heap. Allocate the baseline heap (2g at 2 vCPU, 8g at 4–8 vCPU) and leave the rest of the machine’s RAM to the OS page cache — larger heaps showed no measurable benefit.
Avoid 2 vCPU deployments under sustained concurrent load. P95 exceeds 4 s for both backends.