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: .. list-table:: :header-rows: 1 :widths: 20 30 50 * - 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. .. list-table:: :header-rows: 1 :widths: 8 8 12 12 12 12 12 * - 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: .. list-table:: :header-rows: 1 :widths: 8 8 14 20 15 * - 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. .. toctree:: :maxdepth: 1 Caching