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

perf-sut-2

ccx13 (2 vCPU / 8 GB)

openFHIR, heap 2g / 4g

perf-sut-4

ccx23 (4 vCPU / 16 GB)

openFHIR, heap 8g / 12g

perf-sut-8

ccx33 (8 vCPU / 32 GB)

openFHIR, heap 8g / 16g / 24g

perf-db

ccx13

mongo:7 / postgres:16 (unrestricted)

perf-loadgen

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.