MLLP — the Minimum Lower Layer Protocol — is arguably the most important piece of plumbing in US healthcare integration. Every major EHR (Epic, Cerner, MEDITECH, Allscripts) and every mainstream interface engine (Mirth Connect, Rhapsody, Cloverleaf, Iguana, Ensemble/HealthShare) speaks MLLP. It is simple enough that you can implement it in a weekend, and subtle enough that most production incidents happen because someone misread the specification.
This article is the reference we wish we had when we started debugging MLLP at 2 a.m. — what the bytes actually look like on the wire, how MLLPS (MLLP over TLS) differs from plaintext, what commit acknowledgements mean in v2, how the big four interface engines interpret edge cases, and how to capture and interpret a failing exchange with tcpdump and Wireshark.
If you are building, operating, or auditing HL7 integrations in 2026, this is the protocol you have to understand. For the wider protocol landscape, see HL7 v2 vs v3 vs FHIR and our healthcare interoperability guide.
1. What MLLP Actually Is
MLLP is a framing convention, not a protocol stack. HL7 International published the specification as a companion to HL7 v2.x messaging because TCP alone gives you a byte stream with no natural message boundaries. MLLP fixes that by wrapping each HL7 message in two sentinel bytes.
What MLLP is:
- A small set of control bytes that mark where HL7 messages begin and end on a TCP connection.
- A convention for keeping the TCP connection alive across many messages (long-lived sessions).
- Transport-agnostic: it runs over plaintext TCP, TLS, VPN, or anywhere else you can open a stream socket.
What MLLP is not:
- An authentication layer — MLLP itself has no identity, no user accounts, no tokens.
- A reliability layer — if a message is lost mid-flight, MLLP does not retransmit it; the application must.
- A routing layer — addressing is baked into the TCP socket, not the MLLP block.
- An encryption layer — MLLP in plaintext is readable by anyone with a packet capture; MLLPS wraps TLS around it.
Put another way, MLLP sits at roughly OSI layer 5 (session), with TCP below and HL7 v2 above. It is deliberately thin — the HL7 working group wanted something that existing vendors could implement in a day, and they succeeded. The cost of that simplicity is that every integration engine adds its own tiny interpretation quirks, which we cover in section 6.
2. Block Structure and Framing Bytes
Every MLLP block looks like this:
<VT> HL7 message content <FS><CR>With exact byte values:
<VT>— hex0x0B, decimal 11, ASCII Vertical Tab. This is the start-of-block marker.<FS>— hex0x1C, decimal 28, ASCII File Separator. End-of-block marker.<CR>— hex0x0D, decimal 13, ASCII Carriage Return. Terminates the block.
A real ADT^A01 message on the wire, byte by byte, looks roughly like this:
0x0B
MSH|^~\&|EPIC|UCHEALTH|MIRTH|TACTION|20260421093000||ADT^A01|MSG00001|P|2.5<CR>
EVN|A01|20260421093000<CR>
PID|1||123456^^^UCHEALTH^MR||DOE^JOHN^Q||19720101|M|||123 MAIN ST^^DENVER^CO^80202<CR>
PV1|1|I|2000^2012^01||||0010^ATTEND^ALBERT|||CAR||||ADM|A0<CR>
0x1C
0x0DSegment terminators inside the HL7 body are themselves <CR> (0x0D). That overlap is intentional — HL7 v2 segments end with CR, and MLLP adds one more CR after FS. Three important consequences:
- Never substitute
\\n(LF, 0x0A) for<CR>. Doing so is the single most common reason HL7 messages are silently rejected by strict parsers. - Windows tooling that silently converts line endings (notepad, some scripts) will corrupt MLLP traffic. Use binary-safe tools.
- The final
<FS><CR>sequence is what the receiver uses to know a full message has arrived.
Here is the same ADT message escaped for a shell printf so you can reproduce it locally:
printf '\x0bMSH|^~\\&|EPIC|UCHEALTH|MIRTH|TACTION|20260421093000||ADT^A01|MSG00001|P|2.5\rEVN|A01|20260421093000\rPID|1||123456^^^UCHEALTH^MR||DOE^JOHN^Q||19720101|M\rPV1|1|I|2000^2012^01\r\x1c\r'Notice the double-escaped \\\\& — shell consumes one level, printf consumes another, and the byte on the wire is a single ampersand. This pattern trips up nearly every engineer the first time they use it.
3. MLLP v1 vs MLLP v2 (HL7 Commit Protocol)
Most engineers say "MLLP" and mean MLLP v1 — the original framing convention. HL7 International published a v2 of the spec (formally "Minimal Lower Layer Protocol Release 2") that adds a commit-level acknowledgement layer. Adoption of v2 has always been limited, but it matters in a few vendor-specific integrations.
3.1 MLLP v1 (de facto standard)
Flow:
- Sender frames the HL7 message and writes it to the socket.
- Receiver reads until
<FS><CR>, parses the HL7 body. - Receiver constructs an HL7 ACK (MSH + MSA segments) and writes it back, wrapped in MLLP framing.
- Sender reads the ACK and marks the message delivered.
There is no separate commit step — the application-level ACK is also the commit. If the receiver crashes after reading the bytes but before writing the ACK, the sender will time out and retry, potentially creating a duplicate at the receiver.
3.2 MLLP v2 (HL7 Commit Protocol)
Flow:
- Sender frames the HL7 message.
- Receiver reads the block and immediately writes a commit ACK — a single byte
0x06(ACK) or0x15(NAK) — confirming the block was received and framed correctly. - Sender moves on to write the next message (pipelining) or waits for the HL7 application ACK.
- Receiver later writes the HL7 application ACK (MSH+MSA) after the message has been processed.
A commit ACK on the wire:
0x06 ; ACK (positive commit)
0x15 ; NAK (negative commit — resend)Why v2 exists:
- Clean separation of transport-level "message framed correctly" from application-level "message accepted".
- Allows safe pipelining — sender can push multiple messages without waiting for slow application processing.
- Reduces duplicate risk during receiver restarts.
Why adoption is limited: most vendors implemented only v1, and the HL7 application ACK is usually "good enough" for the volumes and reliability SLAs real-world integrations target. If you see a vendor requirement for MLLP v2, treat it as a configuration flag — both sides must enable it, or the single 0x06 byte will look like garbage to a v1 receiver and break the session.
4. MLLPS — MLLP over TLS
Plaintext MLLP is cleartext PHI on the network. Under the HIPAA Security Rule's transmission security requirements, that is not acceptable on any untrusted segment — and in 2026, most payers and integrators consider it unacceptable even on internal VLANs. MLLPS is simply MLLP wrapped in TLS 1.2 or newer.
4.1 The handshake
At connection time:
- TCP three-way handshake (SYN → SYN/ACK → ACK).
- TLS handshake — client hello, server hello, certificate exchange, session key negotiation.
- Once TLS is established, MLLP framing bytes flow through the encrypted session exactly as in plaintext.
Probing an MLLPS endpoint:
# Inspect the certificate chain
openssl s_client -connect mirth-host:6662 -showcerts
# Check supported protocols
openssl s_client -connect mirth-host:6662 -tls1_2
openssl s_client -connect mirth-host:6662 -tls1_3
# After handshake, paste framed HL7 into stdin to exercise the full stack4.2 mTLS (mutual TLS)
Many hospital-to-hospital and hospital-to-payer MLLPS deployments require mutual TLS — the sender must present a client certificate that the receiver validates. On the Mirth side:
- Import the client CA into Mirth's truststore (
/opt/mirthconnect/conf/truststore.jks). - Enable "Require client certificate" on the MLLP Listener connector.
- Pin the Subject DN or certificate fingerprint if you want strict peer identity.
4.3 Cipher suites and protocol minimums
Recommended baseline in 2026:
- Minimum protocol TLS 1.2, preferably TLS 1.3.
- Disable SSLv3, TLS 1.0, TLS 1.1 at the JVM level.
- Prefer AEAD cipher suites (AES-GCM, ChaCha20-Poly1305).
- Rotate certificates every 12 months with alerts 90/60/30/7 days before expiration.
See our companion Mirth Connect SSL/TLS hardening for the full recipe including JVM flags, keystore layout, and CI certificate validation.
5. ACK Flow Inside MLLP
MLLP carries HL7 messages in both directions. After a sender pushes an ADT^A01, the receiver replies with an ACK — a full HL7 message with an MSA segment indicating accept/error/reject.
A positive ACK on the wire:
<VT>
MSH|^~\&|MIRTH|TACTION|EPIC|UCHEALTH|20260421093001||ACK^A01|ACK00001|P|2.5<CR>
MSA|AA|MSG00001<CR>
<FS><CR>AA = Application Accept. Other MSA-1 values:
AA— Application Accept (success).AE— Application Error (the message was received but processing failed — retry is not expected to help).AR— Application Reject (the message is malformed or unauthorized — do not retry).
Enhanced acknowledgment mode (MSH-15 / MSH-16) adds commit-level codes CA, CE, CR for the initial commit and AA, AE, AR for the application response. For the full decision tree — including the ERR segment and retry logic — see HL7 ACK/NAK explained.
MLLP is strict about one ACK per message — the sender reads exactly one framed block from the socket after writing a framed block. Returning two ACKs, or interleaving messages and ACKs out of order, will break most senders.
6. Mirth, Rhapsody, Cloverleaf, Iguana Interop
All four major interface engines speak MLLP, but they differ in defaults and edge-case behavior. Knowing the differences saves hours when you are integrating across vendors.
6.1 Mirth Connect (NextGen)
- MLLP Listener and MLLP Sender connectors in every channel.
- Defaults: plaintext, port 6661, one connection per sender, ACK auto-generated from the response transformer.
- TLS: enabled per-connector with keystore/truststore references; supports mTLS.
- Framing bytes configurable but rarely changed from 0x0B / 0x1C / 0x0D.
- Known quirk: default read timeout is 5 seconds — long-running receivers need to increase it.
6.2 Rhapsody (Lyniate)
- TCP/IP Comm Point in Sender or Receiver mode.
- Supports MLLP v1 and v2 (commit ACK via the "HL7 Lower Layer Protocol v2" setting).
- Strong TLS configuration UI; integrates with the engine's certificate store.
- Known quirk: Rhapsody sends HL7 ACKs with its own MSH timestamp rather than echoing the sender's — safe but can confuse ACK correlation.
6.3 Cloverleaf (Infor)
- TCP/IP protocol with MLLP framing at the raw protocol level.
- Traditionally plaintext-heavy; MLLPS is available but setup is less GUI-driven than Mirth or Rhapsody.
- Uses tcl-based configuration, which makes bulk changes scriptable but harder for engineers unfamiliar with the engine.
- Known quirk: some older Cloverleaf deployments emit
<CR><LF>after<FS>— non-spec but tolerated by most receivers.
6.4 Iguana (iNTERFACEWARE)
- LLP Listener and LLP Client components.
- Lua-driven scripting surface — very flexible; also very easy to get framing wrong if you write your own ACK logic.
- TLS: supported natively; certificates managed in the Iguana web UI.
- Known quirk: Iguana defaults to Latin-1; senders emitting UTF-8 payloads must explicitly configure the channel encoding.
6.5 InterSystems Ensemble / HealthShare
- EnsLib.HL7.Adapter.TCPInboundAdapter / TCPOutboundAdapter.
- Supports plaintext and TLS; mTLS is possible but configuration lives across several production/service settings.
- Known quirk: Ensemble is strict about the
MSH-9message type in ACKs — a mismatched ACK type can cause it to mark the message as failed even withMSA|AA.
Our broader Mirth Connect issues and fixes catalog and EHR integration guide cover engine-specific gotchas in more detail.
7. Common Implementation Pitfalls
If we had to summarize every MLLP incident we have handled in the last five years, it would be one of the items below:
- !Sending <CR> without <FS> — receiver waits indefinitely for end-of-block marker
- !Mixing <CR><LF> line endings from Windows tooling — MLLP requires <CR> only (0x0D)
- !Character encoding mismatch — sender UTF-8, receiver Latin-1, truncation on special chars
- !Using TLS certificates signed by private CA without updating receiver truststore
- !Firewall or load balancer idle-timeout dropping long-lived MLLP connections silently
- !Not returning an ACK quickly enough — sender times out and retries, creating duplicates
- !Binding listener to 127.0.0.1 instead of 0.0.0.0 so remote senders cannot connect
- !Reusing the same port for plaintext MLLP and MLLPS — senders pick the wrong mode
- !Assuming batch framing (BHS/BTS) is supported when the receiver only handles single messages
- !Sending an HL7 message that exceeds the receiver's buffer size without a negotiated cap
The single most instructive pitfall is line endings. Here is a correct HL7 message with CR-only terminators followed by MLLP framing:
0x0B
MSH|^~\&|LAB|HOSPITAL|MIRTH|HOSPITAL|20260421093000||ORU^R01|MSG123|P|2.5
OBR|1|ORDER123|FILLER456|CBC^Complete Blood Count||20260421090000
OBX|1|NM|WBC^White Blood Cell Count||7.2|10*9/L|4.0-11.0|N|||F
0x1C 0x0DAnd here is the wrong version you will sometimes see when a Windows tool touched the file:
0x0B
MSH|^~\&|LAB|...|ORU^R01|MSG123|P|2.5<CR><LF>
OBR|1|ORDER123|...<CR><LF>
OBX|1|NM|WBC...<CR><LF>
0x1C 0x0DStrict parsers (most of them) reject this. Others silently accept it and then produce garbled ACKs. Always use binary-safe tooling when you are generating or inspecting HL7.
8. Testing MLLP with nc and printf
The fastest way to validate an MLLP listener is a one-line printf | nc pipeline. This exercises the full path — TCP, MLLP framing, HL7 parsing, ACK generation — and returns the ACK bytes directly to your terminal.
8.1 Plaintext MLLP probe
# Build an ADT^A08 message and send it
printf '\x0bMSH|^~\\&|TEST|TEST|MIRTH|TACTION|20260421093000||ADT^A08|MSG00042|P|2.5\rEVN|A08|20260421093000\rPID|1||PID0001^^^HOSP^MR||SMITH^JANE||19850101|F\r\x1c\r' | nc -w 5 mirth-host 6661 | xxdThe xxd pipe is optional — it shows you the raw hex of the ACK so you can confirm the framing bytes are exactly 0x0B at the start and 0x1C 0x0D at the end. If you see anything else, framing is broken.
8.2 MLLPS probe with openssl
# Connect, then paste the printf-generated bytes into stdin
openssl s_client -connect mirth-host:6662 -quiet
# Or scripted — send and exit
printf '\x0bMSH|^~\\&|TEST|TEST|MIRTH|TACTION|20260421093000||ADT^A08|MSG00042|P|2.5\rEVN|A08|20260421093000\r\x1c\r' | openssl s_client -connect mirth-host:6662 -quiet -no_ign_eof8.3 Python one-liner for batch testing
python3 - <<'PY'
import socket, sys
msg = b'\x0bMSH|^~\\&|TEST|TEST|MIRTH|TACTION|20260421093000||ADT^A08|MSG00042|P|2.5\rEVN|A08|20260421093000\r\x1c\r'
with socket.create_connection(('mirth-host', 6661), timeout=5) as s:
s.sendall(msg)
resp = b''
while b'\x1c\r' not in resp:
chunk = s.recv(4096)
if not chunk: break
resp += chunk
print(resp.hex())
print(resp.decode('latin-1', errors='replace'))
PYThis approach scales into integration tests — parameterize the message template and iterate over a fixture set.
9. Debugging with tcpdump and Wireshark
When manual probes succeed but production traffic fails, the answer is almost always on the wire. Capture, then analyze.
9.1 Capture on the Mirth host
# Capture all traffic on the MLLP port to a file
sudo tcpdump -i any -w /tmp/mllp-capture.pcap 'tcp port 6661'
# Rotate files every 100 MB
sudo tcpdump -i any -C 100 -W 10 -w /tmp/mllp-capture.pcap 'tcp port 6661'
# Filter by source host
sudo tcpdump -i any -w /tmp/mllp-capture.pcap 'src host 10.0.1.50 and tcp port 6661'9.2 What to look for in Wireshark
- TCP SYN / SYN-ACK / ACK handshake completes cleanly at the start of each session.
- First application byte after the handshake is
0x0B. If it is anything else, framing is broken. - Before the session ends,
0x1C 0x0Dappears. - In each direction, one block corresponds to one message or one ACK.
- No
RSTpackets — those indicate abrupt session termination.
9.3 Decoding TLS captures
If you are capturing MLLPS, the payload will be encrypted. To decrypt, export the server's private key (or use TLS key-log files from the sender) and load them into Wireshark under Preferences → Protocols → TLS. Key caveats:
- TLS 1.3 sessions are generally undecryptable from the server key alone — you need the session key log.
- Be careful about storing private keys on engineer workstations — treat any exported key as a secret.
- For production debugging, we prefer a non-TLS sidecar (a plaintext staging listener) over decrypting live PHI traffic.
For a broader troubleshooting playbook, see common HL7 integration errors.
10. Production Hardening Checklist
Items every production MLLP deployment should have in place:
- TLS 1.2+ enforced on every internet-facing or cross-organization MLLP link.
- Certificate expiration monitored 90/60/30/7/1 days before
notAfter. - mTLS for any link crossing trust boundaries (hospital ↔ payer, hospital ↔ registry).
- End-to-end synthetic heartbeat — a test message flows every N minutes and triggers alerting if the ACK does not return.
- Port reachability monitored from the actual sender's network perspective, not just from the Mirth host.
- Firewall idle timeouts > MLLP idle period, or explicit TCP keepalives configured.
- Message buffer size aligned on both ends (avoid the "pipeline stall on large ORU" failure mode).
- Channel state alerting — any stopped listener fires a pager.
- Documented runbook for MLLP failures, rehearsed quarterly.
- Every MLLP interface in a central inventory — port, direction, TLS posture, certificate owner, business owner.
For a worked example of an operational maturity program, see our Mirth helpdesk service and the broader Taction services catalog.
11. When Not to Use MLLP
MLLP is the right answer for most HL7 v2 traffic, but there are situations where it is not.
- FHIR workloads — use HTTPS/REST. MLLP adds no value and removes FHIR's native caching, idempotency, and content negotiation. See FHIR integration guide.
- Internet-scale, many-to-many workloads — HTTPS or message buses (Kafka, Azure Service Bus) scale better than long-lived TCP sessions per tenant.
- Bursty, asynchronous workloads with retries and dead-letter semantics — a proper queue (RabbitMQ, SQS) is a better substrate.
- High-frequency device telemetry — protocols like MQTT or HL7 FHIRcast are purpose-built and more efficient.
- Inter-vendor cloud integrations where both sides support FHIR — prefer FHIR for new builds.
That said, MLLP will be relevant for another decade. The installed base of HL7 v2 interfaces is enormous, and migrating to FHIR is a multi-year program. Plan for both.
12. Frequently Asked Questions
What does MLLP stand for?
MLLP stands for Minimum Lower Layer Protocol. It is the de-facto transport for HL7 v2.x messaging over TCP — a lightweight framing convention defined by HL7 International rather than a full protocol stack.
What port does MLLP use by default?
There is no registered default port. Common choices are 6661 (plaintext), 6662 (MLLPS), 9999, 2575, and various application-specific ports agreed between sender and receiver. Always document the port in your interface inventory.
Is MLLP secure?
Plaintext MLLP is not secure — PHI flows in the clear. MLLPS (MLLP over TLS 1.2+) is the current standard for HIPAA-covered environments. If you cannot run MLLPS, run MLLP inside a VPN, IPsec tunnel, or private cloud interconnect.
What is the difference between MLLP v1 and v2?
MLLP v1 relies solely on the application-level HL7 ACK. MLLP v2 (the HL7 Minimal Lower Layer Protocol Release 2) adds a commit-level acknowledgement so the sender knows the receiver framed the message correctly even before the application layer answers. Most production deployments still use v1 semantics.
Why does my MLLP message get truncated?
Almost always a framing issue. The receiver needs the exact sequence <VT>message<FS><CR>. A sender that emits <VT>message<CR> (no <FS>) will hang the receiver until a timeout. Capture bytes with tcpdump and verify hex 0x0B, 0x1C, 0x0D.
Can MLLP carry FHIR messages?
Technically yes — you can wrap any payload inside MLLP framing — but FHIR is designed for REST over HTTPS. Using MLLP to transport FHIR is unusual and generally discouraged outside specific HL7 v2-to-FHIR bridging scenarios.
How do I test MLLP connectivity manually?
Use nc or ncat with a printf-built frame: printf '\x0bMSH|...\r\x1c\r' | nc host 6661. A well-behaved listener will respond with an ACK wrapped in the same framing bytes.
Does MLLP support batching multiple HL7 messages?
MLLP itself frames one message per block (VT...FS CR). HL7 batch protocol (FHS/BHS/BTS/FTS) can carry multiple messages inside a single MLLP block, but most production integrations avoid this and send one ADT/ORU/ORM at a time.
What happens if the MLLP connection drops mid-message?
The receiver discards the partial buffer at the next VT. Well-behaved senders retry the message from their outbound queue. If there is no queue on the sender side, the message is lost — which is one reason store-and-forward buffering belongs at the application layer.
Should I use persistent or per-message MLLP connections?
Persistent (long-lived) TCP connections are the convention. Opening and closing TCP for every message adds latency and wastes resources. Use MLLP keepalives or periodic health probes to detect silently-broken connections.
Related Reading
- HL7 Integration: The Complete Guide
- Mirth Connect: The Complete Guide
- HL7 ACK/NAK Explained
- HL7 v2 Message Structure Explained
- HL7 v2 vs v3 vs FHIR
- Mirth Connect MLLP Connection Refused
- Common HL7 Integration Errors
- HL7 Testing Tools & Sample Messages
- Mirth Connect Java Heap Space Error
- Common Mirth Connect Issues & Fixes
- Healthcare Interoperability Guide