What is MLLP and how does it work
MLLP wraps HL7 messages in three framing bytes and sends them over TCP. How the protocol works, why healthcare still uses it, and what to know before implementing it.
MLLP stands for Minimal Lower Layer Protocol. It wraps HL7 v2 messages in a thin framing envelope and sends them over a persistent TCP connection. Three bytes do all the work: one to mark the start of a message, two to mark the end.
The protocol is defined in the HL7 v2 standard, Appendix C. It has no versioning, no content negotiation, no headers. A sender opens a TCP connection, writes framed messages, and waits for acknowledgments. That’s it.
Why MLLP exists
HL7 v2 was designed in the late 1980s. HTTP didn’t exist yet. The protocol needed a way to send variable-length messages over TCP without a higher-level application protocol to handle framing.
The problem is simple: TCP is a byte stream. If you send two HL7 messages back-to-back, the receiver has no way to know where one ends and the next begins. MLLP solves this by wrapping each message in delimiter bytes.
HTTP solves the same problem with Content-Length headers and chunked transfer
encoding. MLLP predates all of that. It uses fixed byte values instead.
Forty years later, most HL7 v2 traffic still runs over MLLP. Not because it’s the best option — but because the installed base is enormous. Hospital information systems, lab instruments, radiology systems, and pharmacy dispensers all speak MLLP. Replacing the protocol means replacing every endpoint.
The three framing bytes
An MLLP frame has this structure:
0x0B <HL7 message payload> 0x1C 0x0D
| Byte | Hex | Name | Purpose |
|---|---|---|---|
| Start | 0x0B | Vertical Tab (VT) | Marks the start of a message |
| End | 0x1C | File Separator (FS) | Marks the end of the payload |
| Return | 0x0D | Carriage Return (CR) | Terminates the frame |
The receiver reads bytes from the TCP stream until it encounters 0x0B. Everything
after that is payload until it sees 0x1C 0x0D. The bytes between the start
and end delimiters are the HL7 message.
A concrete example. This is an ADT^A01 (patient admission) message wrapped in MLLP framing:
0x0B
MSH|^~\&|HIS|CENTRAL|LAB|PATHOLOGY|20260403120000||ADT^A01|MSG00001|P|2.3
PID|||123456^^^MRN||DOE^JOHN||19800101|M|||123 MAIN ST^^HELSINKI^^00100^FI
PV1||I|ICU^001^01||||12345^SMITH^ANNA
0x1C 0x0D
The 0x0B and 0x1C 0x0D bytes are not part of the HL7 message. They’re
stripped by the MLLP layer before the message is parsed.
Inside the payload, each HL7 segment is separated by 0x0D (carriage return).
This is an HL7 convention, not an MLLP one. The same 0x0D byte serves double
duty: segment delimiter inside the message, and frame terminator after 0x1C.
For a deeper look at the framing bytes and edge cases, see MLLP framing bytes: 0x0B, 0x1C, 0x0D.
The ACK/NAK handshake
MLLP is synchronous. The sender writes a framed message, then waits for a response on the same connection before sending the next one. The response is itself an MLLP-framed HL7 message — specifically, an ACK message.
The ACK contains a field called the acknowledgment code. Three values matter:
| Code | Name | Meaning |
|---|---|---|
AA | Application Accept | Message received and processed |
AR | Application Reject | Message rejected. Permanent. Don’t retry. |
AE | Application Error | Processing error. Transient. May retry. |
An ACK message looks like this:
MSH|^~\&|LAB|PATHOLOGY|HIS|CENTRAL|20260403120001||ACK^A01|ACK00001|P|2.3
MSA|AA|MSG00001
The MSA segment carries the acknowledgment code (AA) and the control ID of
the original message (MSG00001). This is how the sender correlates the
response with the request.
The distinction between AR and AE matters for retry logic. AR means the
message itself is wrong — a missing required field, an invalid value, a
malformed segment. Retrying won’t help. AE means something went wrong on
the receiving side — a database timeout, a downstream system unavailable.
Retrying might succeed.
See HL7 ACK and NAK codes: AA, AR, AE explained for detailed examples of when each code applies.
Connection lifecycle
An MLLP connection is a persistent TCP connection. The typical pattern:
- The sender opens a TCP connection to the receiver
- The sender writes an MLLP-framed message
- The receiver reads the frame, processes the message, and writes an ACK
- The sender reads the ACK
- Steps 2-4 repeat for each message
- Either side closes the connection when done
There’s no connection negotiation. No handshake beyond TCP’s three-way
handshake (and TLS, if configured). The first byte the receiver sees after the
connection opens should be 0x0B.
Most implementations keep connections open for extended periods. A lab instrument might open a connection at boot and send results over it for days. An interface engine might maintain a pool of persistent connections to each downstream system.
This is different from HTTP, where each request-response pair is conceptually independent (even with keep-alive). MLLP connections carry state: if a sender is waiting for an ACK, the connection is busy. Only one message can be in-flight per connection at a time.
Timeouts
MLLP itself doesn’t specify timeouts. Implementations add them to handle real-world failure modes:
Idle timeout. Close connections that haven’t sent data for a configurable period. Prevents abandoned connections from consuming resources. A sender that crashed without closing the socket will be cleaned up after the idle timeout expires.
Frame timeout. Limit how long the receiver waits for a complete frame.
If 0x0B arrives but 0x1C 0x0D doesn’t follow within the timeout, the
receiver closes the connection. This protects against slow-loris style attacks
and misbehaving senders.
Write timeout. Limit how long the receiver spends writing the ACK. A sender that stops reading (perhaps it crashed between sending and receiving) will cause the write to block. The write timeout bounds this.
TLS and mTLS
MLLP runs over TCP. Adding TLS is straightforward — the MLLP framing doesn’t change, it just runs inside an encrypted tunnel. The receiver listens for TLS connections instead of plain TCP. Everything else stays the same.
Mutual TLS (mTLS) adds client certificate verification. The receiver checks that the sender presents a certificate signed by a trusted CA. This is common in healthcare, where both sides of an interface need to prove their identity.
The HL7 standard doesn’t mandate TLS for MLLP. Many hospital networks still run MLLP over plaintext TCP inside a private VLAN. Whether that’s acceptable depends on your security posture and regulatory environment. HIPAA requires encryption of PHI in transit, but allows exceptions for networks with “equivalent protections.”
MLLP vs HTTP for HL7
HL7 FHIR uses HTTP. HL7 v2 traditionally uses MLLP. The protocols solve different problems and the choice usually isn’t yours — it’s determined by what the systems on each end support.
| Aspect | MLLP | HTTP |
|---|---|---|
| Connection model | Persistent TCP | Request-response |
| Messages per connection | Many (serial) | One per request (typically) |
| Framing | Byte delimiters | Content-Length / chunked |
| Routing | IP and port | URL path, headers |
| Load balancing | TCP-level (sticky sessions) | HTTP-level (any request) |
| Observability | Custom | Standard HTTP metrics |
| TLS | Application or network layer | Standard HTTPS |
The practical differences show up in operations. HTTP load balancers, API gateways, and service meshes all understand HTTP semantics. MLLP connections are opaque TCP streams to this infrastructure. Load balancing MLLP requires session affinity — routing all messages from a sender to the same receiver — because the protocol assumes a persistent, stateful connection.
For a detailed comparison, see MLLP vs HTTP for HL7 messaging.
Common deployment patterns
Standalone binary. A single process listening on a TCP port. Configuration through environment variables. Suitable for bare metal, VMs, or containers. The simplest deployment: one binary, no dependencies.
Docker container. The same binary in a container image. Expose port 2575, pass environment variables for configuration. Health checks use TCP socket probes — MLLP has no HTTP endpoint to check.
Kubernetes. Deployment with Kustomize overlays for environment-specific configuration. Key considerations: TCP health probes (not HTTP), session affinity on the Service, pre-shutdown delay for endpoint propagation, and NetworkPolicy to restrict which namespaces can reach the MLLP port.
The deployment model doesn’t change how MLLP works. The protocol is the same whether the process runs on a Windows server in a hospital basement or a pod in a managed Kubernetes cluster.
Validating messages
A receiver can validate incoming messages before acknowledging them. The
validation result maps directly to ACK codes: if validation passes, return
AA. If a rule fails, return AR with the reason. If validation itself
errors, return AE.
Validation rules check parsed fields from the HL7 message. Typical rules:
- Require a patient ID in PID-3
- Require a sending facility in MSH-4
- Restrict message types to a known set
- Verify that observation values are within expected ranges
The rules run at receive time, before the message is routed to any downstream
system. A bad message is rejected at the front door — the sender gets an AR
ACK and knows immediately what’s wrong.
See CEL expressions for HL7 message validation for a practical guide to writing validation rules.
MLLP Server is an open source HL7 MLLP server with CEL validation, TLS/mTLS, and message routing. View the documentation.