How MessageFoundry fits together
Four views, each answering a different question: every shipped component and how it relates to the engine; the engine's internal packages and where the process boundaries sit; how a received message moves through the durable staged queue and earns a disposition; and how Connections, Routers, and Handlers wire together by name. The prose source of truth is the engine's ARCHITECTURE.md.
This architecture is real and running — but the project is young. MessageFoundry started in May 2026 and is in Early Access. See where it stands →
Top-level components
Beyond the engine itself, MessageFoundry ships as a set of independent, separately-built components — operator tools, the version-controlled config, the CLI and Windows service that run it, dev/test tooling, a standalone migration tee relay, and the CI-to-PyPI release path. Operator tools reach the engine only through the localhost API; the engine is the core, and everything else sits around it.
System topology
The engine is a headless service; clients are separate processes that reach it only through the localhost API. Dependencies point one way — the API depends on the engine, the engine never imports the API or a client.
The durable staged queue
A received message is ACKed on receipt — once it's durably committed to the ingress stage, before routing, transform, or delivery. Each handoff is a single committed transaction, giving reliable delivery, retries, and replay with no separate broker.
Why nothing is lost. Every message is committed to the store before it's ACKed, and each stage handoff is a single transaction — so a crash can only ever repeat the last step, never skip it. In normal operation every message is delivered exactly once. The one edge case: if the engine crashes after a downstream send but before it records that success, on restart it re-sends that single in-flight message rather than risk dropping it. (Engineers call this "deliver again rather than lose" guarantee at-least-once — the deliberate opposite of at-most-once, which can silently lose clinical data.) That rare re-send is detectable, not noise: a re-delivered HL7 message carries the same MSH-10 control ID, so a downstream sees a retry of a known message — exactly where deduplication belongs.
How much one interface carries. Capacity is a measured property of this staged queue, not a separate claim. On anonymized, representative message traffic, a single inbound interface sustains a measured ~50 messages/second on commodity hardware — about 180,000 in a peak hour and on the order of 1.6 million a day — with every message committed to the durable store before it's ACKed and delivered strict per-interface FIFO. Whatever the rate, nothing is counted that isn't stored, and nothing is silently dropped.
Those day-level figures are sized to the busy hour, not a flat 24 hours: clinical ADT traffic is business-hours-bursty. In de-identified ADT traffic profiled from a live hospital interface feed, the busiest hour runs roughly 2.7× the daily average, at around 11.5 KB per message — so one interface moves on the order of 18 GB/day. Treat the message size as directionally right rather than a constant: it varies a great deal by feed, and messages carrying base64-encoded documents or images are far larger, which changes the storage and bandwidth arithmetic well before it changes the message rate. A single hospital's full ADT feed is only a few percent of one interface, so capacity is rarely the question for one site — and when it is, you scale horizontally: feeds split at the source across more interfaces, plus an optional multi-node tier. At that grain, 16 million messages a day is roughly 10 interfaces — distributed, not forced through a single pipe. Your real numbers track your transforms, storage, and hardware. How to read these throughput numbers → · Throughput & capacity (PDF) ↓
Connections, Routers, and Handlers — wired by name
Your configuration is a graph wired by name — a set of nodes connected by edges, like boxes joined by arrows in a flowchart. An inbound Connection names a Router; the Router forwards to Handler(s) by name; each Handler sends to outbound Connection(s). There's no enclosing "channel" object — set it up with guided wizards, or write it in Python.
A wiring diagram, not a stack of channels
Because the links are just names, each box is defined once and the arrows are cheap — what a bundled "channel" can't give you.
- Define once, reuse anywhere — name a destination, a shared transform, or a router and reference it from anywhere, instead of copying it into every channel that needs it.
- Rewiring is a one-line diff — add a destination with one more Send, or reroute a feed by changing a name. No "channel" surgery, and every change is a small version-controlled diff.
- Mistakes caught at load — an unknown router, a dangling handler, a duplicate name or port are errors at check time, not silent surprises in production.
- Each piece is small and testable — a Router or Handler is a tiny function with a clear contract, so your team (or an AI agent) can build and test parts in parallel.
See it run for yourself
From install to your first routed message — with the full feature set and the engine docs a click away.