Sizing guidance
This page is deliberately short on numbers. Meridian ships as four small processes (see Deployment topology) with no cluster manifests and no load-tested capacity figures in this repository — there is no Kubernetes deployment or resource-limits block to copy sizing numbers from. What follows is the reasoning an operator needs to decide which of those processes to scale, and when, rather than a table of CPU/RAM numbers to paste into a manifest.
The load is I/O-bound, not CPU-bound
Almost all of the wall-clock time a workflow run spends is spent waiting on
something outside the Meridian processes: a FHIR call, a terminology lookup,
an LLM completion. The trace shape documented under "Request flow" in
Deployment topology makes this visible directly —
each node span nests the external calls it made (fhir GET|POST|PUT,
posos.graphql, …) under itself, and in practice those calls dominate the
span duration, not the interpreter's own work walking the WorkflowSpec.
The interpreter itself is a plain dataflow walk over a graph
(see Interpreter internals)
with no heavy computation of its own.
The practical consequence: adding CPU or replicas to endpoint or proxy
rarely helps if a run "feels slow." The first thing to check is whether the
adapters they call — the FHIR warehouse, the terminology server, the LLM
provider — are the ones under pressure. Capacity planning for an instance
starts with those external systems, not with the four Meridian processes.
Four stateless processes, one lever each
None of the four processes hold application state of their own (see the "Processes" table in Deployment topology) — the durable state of every run lives in Restate, and the reasoning for that split is covered in Why durable execution. Statelessness is what makes each process replicable at all; whether it's worth replicating is a separate question, answered differently for each one.
endpoint — the workflow engine
Because a run's durable state is Restate's, not the engine's, endpoint can
in principle be replicated: point RESTATE_ENDPOINT_URL at whatever address
fronts your replicas (a load balancer, a Kubernetes Service) and any
replica can pick up any invocation, since nothing about a run's progress is
pinned to a specific endpoint process. This repository's docker-compose.yml
runs a single endpoint; scaling it out further is an operational decision
this repo doesn't need to make for itself, not a redesign.
What actually determines endpoint's footprint is less fixed than it looks.
Plugins execute in-process and unsandboxed — there is no per-plugin resource
isolation (see
Plugin host loading
and Autonomy and packaging).
So "how big does endpoint need to be" is really "how big is the loaded
plugin set, and what does it do" — a plugin whose behaviors buffer large LLM
responses or do heavier client-side processing changes the answer, and the
platform itself contributes only a thin baseline on top. Sizing endpoint
is sizing your instance's plugins, not the platform.
One more detail worth knowing rather than tuning around: the image runs
TypeScript sources through tsx rather than an ahead-of-time compiled
binary (see the Dockerfile comment on this and the
packaging trajectory), so a freshly started
replica pays a small transpilation cost on boot compared to a precompiled
process. That's a cold-start detail, not a steady-state one — it only
matters if you're scaling endpoint up and down on a very short cycle.
proxy — the REST API
proxy is stateless in the same sense, with one exception: the run index
(see Run index database). The default backend
is a local SQLite file, which by construction only works for a single
proxy replica — two processes writing to the same file is not a supported
configuration. That's the actual gate on proxy horizontal scaling, not CPU
or memory: switch RUN_DB_URL to PostgreSQL (see
Use the Postgres run index) and
multiple proxy replicas become safe, because they now share one database
instead of each keeping a private file.
It's worth being clear about what that decision is actually for. The run
index is a convenience list, not the source of truth for run state — losing
or rebuilding it never loses a run (again, Why durable
execution). So moving to Postgres is usually
about availability of the API (surviving a proxy restart without a gap,
running more than one replica behind a load balancer) rather than about
proxy being CPU-bound at any realistic request rate; each request does
very little computation of its own.
Inbound webhooks
One proxy request shape does hold resources for a long time: a
synchronous webhook (see Trigger a workflow with a
webhook). It keeps the HTTP
connection open until the run settles or its delay expires — up to 110 s — and
polls the run while it waits, with a backoff from 200 ms to 2 s. So N
concurrent synchronous calls cost N held connections plus a bounded trickle
of ingress reads, and a slow workflow turns caller patience into proxy
occupancy.
Asynchronous mode has none of that shape: it acks immediately and the caller
fetches resultUrl when it wants to. Prefer it for anything that isn't
quick, and reserve synchronous mode for calls the caller genuinely has to
block on. Request bodies are capped at 1 MiB (413 beyond), so a large
payload is refused rather than buffered.
restate
Restate is a single binary by design — the docker-compose.yml comment
notes this is meant to be simple to operate, "including in an on-prem
hospital deployment." This repository's stack runs it as one node; there is
no cluster configuration here, and Restate's own clustering/HA story (should
you need it) is Restate's concern, not something this repository configures
or documents. Because Restate is where durable state and invocation
scheduling actually live, it's the piece to watch as real load grows — not
endpoint or proxy CPU usage. The operator runbook this page descends
from characterized a single Restate node as comfortably handling many
thousands of concurrent runs for a hospital's typical clinical-event
volumes; treat that as an order-of-magnitude expectation the platform was
designed around, not a number benchmarked in this repository. If you need to
verify it for your own volumes, the metrics below are the way to do it, and
Back up and restore covers what grows in
the restate-data volume over time.
ui
A stateless Next.js console whose server-side rendering only calls proxy
(see the ui service in Deployment topology). It
scales like any other stateless web frontend; nothing about Meridian's model
changes that.
The real ceiling is usually the adapters, not the platform
Ports delegate to adapters contributed by plugins (see Configure ports and adapters), and terminology resolution works the same way per code system (see Configure terminology). Combined with the I/O-bound argument above, this means an instance's actual capacity ceiling is usually set by systems this repository doesn't provision at all: the hospital's FHIR warehouse, its terminology server, the configured LLM provider's throughput and rate limits. Sizing an instance well means sizing those, and giving the four Meridian processes enough headroom that they are never the bottleneck in front of them — which, per the argument above, is the common case, not the exception.
Measure before you scale
Rather than provisioning against a guess, use the signals the platform
already emits. clinical.nodes.duration and clinical.restate.ingress.duration
show where wall-clock time actually goes per node and per event;
clinical.http.server.duration shows it for proxy's own request handling.
See Observability signals for the
full inventory and Enable observability
for wiring up the local Tempo/Prometheus/Loki/Grafana stack to look at them.
Let a real trace tell you which process — or, more likely, which adapter
behind it — is actually the tail, before adding replicas to something that
isn't.
What this page deliberately leaves out
Which ports must never be publicly reachable is a security posture question, not a sizing one — don't conflate "can this process have more than one replica" with "should it be reachable from outside the cluster." And, as noted at the top, this repository doesn't ship Kubernetes manifests or resource-limit blocks to hand out concrete CPU/memory numbers from; Deployment topology remains the source of truth for the process list and images themselves, and production infrastructure for a given deployment is out of this repository's scope.
Related documents
- Deployment topology — the process list, ports, and images this page reasons about.
- Why durable execution — why Restate, not
endpointor the run index, is the source of truth that makes replication safe. - Run index database and Use the Postgres run
index — the one concrete gate on
proxyhorizontal scaling. - Environment variables — every
variable mentioned above (
RESTATE_ENDPOINT_URL,RUN_DB_URL,PLUGINS_PATH, …). - Observability signals and Enable observability — measuring load instead of guessing at it.
- Back up and restore — what accumulates
in
restate-dataover time. - Security posture — which processes and ports must stay unpublished, independent of how many replicas they have.
- Plugin host loading
and Autonomy and packaging
— why plugins, not the platform, drive
endpoint's real footprint. - Interpreter internals — why the engine's own per-node cost is small relative to adapter calls.