For AI agents: the documentation index is at /docs/interbox/llms.txt. A Markdown version of this page is available at /docs/interbox/ops/sizing.md or by requesting it with the Accept: text/markdown header.
Interbox Docs

System Requirements

Interbox is not a single process. A deployment is three parts, sized separately:

  • The Interbox container — the engine (MLLP ingest + pipeline workers) and the API (which also serves the dashboard). Two Bun processes in one image.
  • PostgreSQL — a separate service. The message store, the work queue, and the search index all live here.
  • Aidbox — a separate JVM service, the FHIR destination the reference pipeline sends to.

PostgreSQL and Aidbox are the resource-heavy parts; the Bun container is light.

These are code-derived estimates, not published benchmarks — from the engine's batch/queue constants and the Postgres tuning shipped in the template. You will not know your real message volume before go-live, and that is fine: pick a starting tier, then watch a few signals and turn a knob when one goes red (signals & levers).

What drives the numbers

Before the tables, the four things that actually move the needle:

  1. Disk grows without bound. There is no time-based retention and no partition rotation — processed history piles up in the cold partitions forever. Disk, not CPU or RAM, is the first thing you will run out of. Size it for your retention horizon (see Disk).
  2. The trigram search index is the write cost. Every inbound message maintains GIN trigram indexes on the raw message body and the patient name. That is what makes the free-text search in the dashboard instant, and it is the dominant per-insert cost on the Postgres side. It also needs an SSD.
  3. Autovacuum runs hot on purpose. The hot queue partitions are tuned to vacuum aggressively (autovacuum_vacuum_cost_delay = 0), which keeps the working set lean but spends IO and CPU to do it. Give Postgres IO headroom.
  4. The dashboard facet rail is a RAM burst. Opening the messages view fires six parallel GROUP BY queries, each allowed up to FACET_WORK_MEM (default 128MB) of work_mem. One dashboard load can therefore ask for ~768 MB of transient sort/hash memory on top of the connection baseline.

Component budgets

Interbox container (Bun)

Light, and not the bottleneck. Baseline is roughly 50–150 MB RSS per Bun process; the engine adds one OS worker thread per pipeline stage (the reference source → mapper → sender pipeline is three). At the ~1 msg/s a "busy feed" implies, CPU is idle most of the time — batches are 100 rows in the mapper and 500 in the sender.

Two things to account for beyond steady state:

  • Boot does real work. For a remote workspace the engine does a shallow git clone + bun install + bun build on startup (and on every reload). That is a transient CPU spike, a few hundred MB, and requires outbound network plus a writable temp dir.
  • The image carries the dashboard assistant SDK. Factored into the ~1 GB image size, not into steady-state RAM.

Budget: 0.5–1 GB RAM, 1–2 vCPU.

PostgreSQL

The scaling axis for both CPU and disk. Cost centers: trigram GIN index maintenance on insert, aggressive autovacuum on the hot partitions, and the facet rail's parallel aggregates. Connection load is the API pool (max: 16) plus a cancel pool (max: 4) plus one connection per engine worker plus the heartbeat — comfortably under the default max_connections = 100 for a single node.

The template ships only max_wal_size = 4GB, wal_compression = on, and pg_stat_statements; shared_buffers is left at the Postgres default of 128 MB, which is too low for production. Tune it per tier below.

Budget: 4–8 GB RAM, 2–4 vCPU at moderate load, on SSD.

Aidbox

The dominant RAM consumer: a JVM FHIR server doing resource validation on every transaction Bundle the sender posts. Its footprint is set by the Aidbox image, not configured by Interbox.

Budget: 4 GB+ RAM, 2 vCPU minimum.

Aidbox defaults its terminology engine to the external server tx.health-samurai.io. That is an outbound network dependency at runtime unless you switch it to a local terminology engine.

Disk: the first limit

There is no retention job. Plan disk for how long you keep history, not for a steady state. As a planning figure, budget ~8 KB per stored message — the raw HL7v2 body plus its trigram GIN index plus the btree indexes.

Sustained feed rateMessages / yearDisk / year @ ~8 KB
0.1 msg/s (light)~3.2M~25 GB
1 msg/s ("busy feed")~31.5M~250 GB
10 msg/s (high)~315M~2.5 TB

For reference, a 100M-row history is roughly 800 GB — a realistic prod-scale figure to plan around. Use SSD: the trigram search and the aggressive autovacuum both punish spinning disk.

Sizing tiers

Whole-node figures, assuming all three parts share a host (split them across hosts for the larger tiers).

TierUseAppPostgresAidboxNode totalDisk
Dev / evalOne laptop, all-in-one, poke it0.5 GB1 GB4 GB8 GB / 4 vCPU20 GB
Small prodOne busy feed, ~1 msg/s1 GB4 GB4 GB12 GB / 4–6 vCPU250 GB+/yr
Medium prodSeveral feeds, large searchable history1–2 GB8–16 GB6–8 GB24–32 GB / 8 vCPU1 TB+ SSD

Bottleneck order, worst first: disk (unbounded) → Postgres CPU (GIN + autovacuum + facets) → Aidbox RAM → the Bun container.

PostgreSQL tuning per tier

The template sets max_wal_size, wal_compression, and shared_preload_libraries on the server, and the API sets facet work_mem per query via the FACET_WORK_MEM environment variable. Everything else is a Postgres default you should raise for production.

Set effective_cache_size to roughly 75% of the RAM you give Postgres (it is a planner hint, not an allocation), and keep FACET_WORK_MEM × 6 × concurrent dashboard loads comfortably under free RAM — that product is the facet rail's peak transient memory.

Small prod (Postgres with ~4 GB):

shared_buffers = 1GB
effective_cache_size = 3GB
maintenance_work_mem = 512MB
max_wal_size = 4GB            # already set by the template
# FACET_WORK_MEM env var: 128MB (default) is fine

Medium prod (Postgres with ~8–16 GB):

shared_buffers = 4GB
effective_cache_size = 12GB
maintenance_work_mem = 1GB
max_connections = 200         # headroom for more engine workers
max_wal_size = 8GB
# FACET_WORK_MEM env var: 128–256MB

For write-heavy feeds, put the WAL on a separate volume from the data and give autovacuum room — it is deliberately uncapped on the hot partitions.

Picking a starting point

You will not know your real message volume before go-live. Don't try to — HL7 feeds are low-rate (even a busy hospital ADT feed is single-digit messages per second), so for a single feed CPU and RAM are almost never the constraint. Pick by feed count, over-provision a little because it is cheap, and tune the rest reactively once real traffic shows you where it pinches:

  • Just evaluating → Dev / eval.
  • One feed, no heavy history search → Small prod.
  • Several feeds, or heavy dashboard search over large history → Medium prod.

The one thing you cannot wing is disk. It grows forever — there is no retention — and under-sizing it is the painful mistake to fix late. Decide the disk size and a retention/archival plan up front from the disk table; treat everything else as adjustable.

Once it's running: signals and levers

You find your real numbers in production, from the dashboard and Postgres — not from a load test nobody has time to run. Watch these; when one goes red, turn the matching knob.

You see (dashboard / Postgres)Most likely meansKnob to turn
Queue depth (pending + received) climbingingest outpaces processingmore engine CPU — but check Aidbox latency first
Aidbox Bundle POST latency or heap highthe FHIR server downstream is the bottleneckgive Aidbox RAM/CPU; lower the sender batch size
Dashboard search / facets slowwork_mem too low, or history is hugeraise FACET_WORK_MEM; keep the 24h window
Postgres writing temp files (sort/hash spills)work_mem too low for the queryraise work_mem / FACET_WORK_MEM
Disk filling uphistory piling up — there is no retentionadd disk now; start a retention/archival plan
Postgres CPU high, dead tuples risingautovacuum can't keep upmore Postgres CPU/IO (already tuned aggressive)

The single most telling signal is queue depth: if the pending and received row counts stay flat you are keeping up; if they climb, processing cannot match ingest — and the cause is almost always Aidbox downstream, not the engine.

Caveats

  • Estimates, not benchmarks — size from the tiers above, then adjust from the live signals once real traffic arrives.
  • The Helm chart lives in a separate helm-charts repository and currently ships replicas = 1; HA is not yet available.
  • Disk figures assume ~8 KB/message — a wide message body or extra indexes moves it. Measure your own average once you have real traffic.

Last updated: