Performance
This page summarises the results of performance testing conducted on openFHIR Enterprise. 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.
All tests run under sustained concurrent load using Apache JMeter: ten thread groups (five mapping domains × toFHIR / toOpenEHR) of 20 threads each — 200 concurrent threads — with a 30-second ramp-up and 100 iterations per thread group, 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:
Role |
Server |
Runs |
|---|---|---|
|
ccx13 (2 vCPU / 8 GB) |
openFHIR, heap 2g / 4g |
|
ccx23 (4 vCPU / 16 GB) |
openFHIR, heap 8g / 12g |
|
ccx33 (8 vCPU / 32 GB) |
openFHIR, heap 8g / 16g / 24g |
|
ccx13 |
mongo:7 / postgres:16 (unrestricted) |
|
ccx23 |
JMeter |
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 without a single error.
vCPU |
Heap |
DB |
Mean (ms) |
Median (ms) |
P90 (ms) |
P95 (ms) |
|---|---|---|---|---|---|---|
2 |
2 GB |
MongoDB |
1 557 |
1 166 |
3 143 |
4 184 |
2 |
2 GB |
PostgreSQL |
1 550 |
1 186 |
3 075 |
4 108 |
2 |
4 GB |
MongoDB |
1 506 |
1 126 |
3 045 |
4 084 |
2 |
4 GB |
PostgreSQL |
1 488 |
1 122 |
2 976 |
3 979 |
4 |
8 GB |
MongoDB |
310 |
71 |
899 |
1 322 |
4 |
8 GB |
PostgreSQL |
288 |
58 |
840 |
1 297 |
4 |
12 GB |
MongoDB |
369 |
103 |
1 043 |
1 515 |
4 |
12 GB |
PostgreSQL |
312 |
83 |
878 |
1 282 |
8 |
8 GB |
MongoDB |
28 |
15 |
50 |
72 |
8 |
8 GB |
PostgreSQL |
23 |
14 |
44 |
58 |
8 |
16 GB |
MongoDB |
25 |
14 |
45 |
63 |
8 |
16 GB |
PostgreSQL |
28 |
14 |
49 |
72 |
8 |
24 GB |
MongoDB |
26 |
14 |
48 |
70 |
8 |
24 GB |
PostgreSQL |
25 |
14 |
46 |
62 |
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 |
85.9 |
1 166 |
2 |
2 GB |
PostgreSQL |
85.7 |
1 186 |
2 |
4 GB |
MongoDB |
88.6 |
1 126 |
2 |
4 GB |
PostgreSQL |
88.7 |
1 122 |
4 |
8 GB |
MongoDB |
165.6 |
71 |
4 |
8 GB |
PostgreSQL |
165.2 |
58 |
4 |
12 GB |
MongoDB |
164.9 |
103 |
4 |
12 GB |
PostgreSQL |
169.1 |
83 |
8 |
8 GB |
MongoDB |
193.7 |
15 |
8 |
8 GB |
PostgreSQL |
187.3 |
14 |
8 |
16 GB |
MongoDB |
185.3 |
14 |
8 |
16 GB |
PostgreSQL |
184.0 |
14 |
8 |
24 GB |
MongoDB |
191.5 |
14 |
8 |
24 GB |
PostgreSQL |
191.1 |
14 |
The highest sustained throughput observed was 194 req/s (8 vCPU / 8 GB heap / MongoDB). At 2 and 4 vCPU the engine is the limit: 86–89 req/s and 165–169 req/s respectively, with openFHIR using all of its CPU. At 8 vCPU it is not: openFHIR used on average less than half of its CPU while the 4-vCPU load generator ran close to its own limit, so the 184–194 req/s measured there is a lower bound of what the engine sustains, not its ceiling. What 8 vCPU buys under this load is latency — a median of 14–15 ms instead of 58–103 ms.
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 ~11–13× (310 ms → 28 ms with MongoDB, 288 ms → 23 ms with PostgreSQL) and collapses the tail — P95 drops from ~1.3 s to 58–72 ms. Mapping is a CPU-bound workload.
Heap size has no measurable effect. At every box size, larger heaps perform within run-to-run noise of the smallest one (8 GB vs 24 GB heap at 8 vCPU: 14–15 ms median for both). 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 and PostgreSQL perform the same. At 8 vCPU both have a 14–15 ms median and a 58–72 ms P95; at 4 and 2 vCPU they are within run-to-run noise of each other. The choice of database can be made on operational grounds.
toFHIR and toOpenEHR behave comparably. Both directions show the same latency at every size and with either database.
Recommendations
Minimum production configuration: 4 vCPU with an 8 GB heap, MongoDB or PostgreSQL — 165 req/s sustained throughput with a 58–71 ms median and 1.3 s P95.
Latency-sensitive deployments: 8 vCPU — 14–15 ms median / 58–72 ms P95 with either database.
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 is around 4 s for both backends.