Sending HL7 v2 over MLLP

Deliver HL7 v2 messages to clinical systems, with each acknowledgment back as a Python object.

An outgoing MLLP connection is a named pool of connections to one remote endpoint - an EHR, a lab system, anything that listens for HL7 v2. Services send through it with self.mllp and receive the remote system's acknowledgment back as a Python object, while every exchange lands in the audit log.

This page shows how to create the connection, send from a service, read the acknowledgment and turn on TLS. For the receive side - accepting messages from clinical systems - go to the receiving guide.

How a message travels

The connection frames the message, sends it and reads the reply, checking that its MSA-2 echoes the control ID of the message it answers - a reply naming another message is reported as an error rather than trusted. Both the message and its acknowledgment are recorded in the audit log.

Create the connection

The Dashboard steps and the enmasse YAML below create the same connection - named EHR Main, pointing at 10.20.30.40:2575:

Go to Connections ▹ Outgoing ▹ HL7 ▹ MLLP, click Create a new connection and complete the wizard:

  1. Name: EHR Main
  2. Address: 10.20.30.40:2575
  3. Go through the delivery options to the review page and click Save

Is anything listening? checks the address before anything is saved - it opens a connection to the endpoint, with TLS when one is configured, and reports whether something accepted it.

The same connection defined in an enmasse file, which is how version-controlled deployments create it:

outgoing_mllp:

  - name: EHR Main
    address: 10.20.30.40:2575

A name and an address are all a connection needs - every other field has a default that matches the MLLP standard. The full list is in Connection fields below.

Send from a service

Services reach the connection through self.mllp, by name. The send method accepts ER7 text or bytes and returns an AckResult with the outcome:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# #####################################################################
# #####################################################################

class SendADT(Service):
    """ Sends an admission message to the EHR.
    """

    def handle(self) -> 'None':

        # The message to deliver ..
        data = (
            'MSH|^~\\&|ZATO|HOSP|EHR|HOSP|20260315120000||ADT^A01|MSG00001|P|2.9\r'
            'EVN|A01|20260315120000\r'
            'PID|1||12345^^^HOSP^MR||SMITH^JOHN||19800115|M\r'
            'PV1|1|I'
        )

        # .. send it through the connection created above ..
        conn = self.mllp['EHR Main']
        result = conn.send(data)

        # .. and log what the EHR answered.
        if result.is_accepted:
            self.logger.info(f'EHR Main answered {result.ack_code} for MSG00001')
        else:
            self.logger.warning(f'EHR Main refused MSG00001: {result.ack_code} {result.error_text}')

A delivered message produces one log line:

INFO - EHR Main answered AA for MSG00001

Each call to send takes a connection from the pool for the duration of the exchange and returns it afterwards. A name that does not exist raises KeyError, and when no acknowledgment arrives within the timeout, the connection raises an exception with the reason - the audit log files such an exchange under Outstanding, together with the message that is still waiting for its reply.

Read the acknowledgment

The AckResult fields - ack_code, is_accepted, should_retry, error_text and ack_text - are listed on the acknowledgments page, together with what each code means in both directions. The two that decide what happens next are is_accepted, which reports that the message arrived, and should_retry, which reports that the remote system asked for the same message again later.

Retries and pauses

A failed send is retried five times by default, with the delay before each attempt doubling from one second up to 300, plus a 10% random jitter that keeps many senders from retrying at the same moment. When half of the sends within a rolling 60-second window fail, sending to the endpoint pauses for 60 seconds before a trial message goes through again. Each of these numbers is a connection field, listed with the others in Connection fields below.

A direct send from a service reports its outcome at once and leaves the retry decision to your code - should_retry carries what the remote system asked for.

The invoke screen

Every connection in the Connections ▹ Outgoing ▹ HL7 ▹ MLLP list has an Invoke link for sending test messages, no code required. Paste an ER7 message, adjust the timeout if needed - it starts as the connection's Invoke page timeout - and click Submit. The response area shows the AckResult the remote endpoint's reply produced:

AckResult(ack_code='AA', is_accepted=True, should_retry=False, error_text='')

TLS and mTLS

Three fields, each a path to a PEM file, control TLS:

FieldWhat it does
CA bundleThe bundle the remote server's certificate is verified against - setting it turns TLS on.
Client certificateThe certificate presented to the server, for mutual TLS.
Client keyThe private key matching that certificate.

With the CA bundle alone, the connection uses TLS and verifies the server. With the certificate and key as well, it presents them back - mutual TLS. The minimum protocol version is TLS 1.2, and with no CA bundle the connection is plaintext.

Connection fields

Every field of an outgoing connection, with its default:

FieldDefaultWhat it does
Name-Identifies the connection - services send through it with self.mllp[name].
ActiveonServices cannot look up an inactive connection.
Address-The remote endpoint as host:port, e.g. 10.20.30.40:2575.
Start bytes0bThe bytes each frame opens with, in hex.
End bytes1c 0dThe bytes each frame closes with, in hex.
Max reply size2097152The biggest acknowledgment accepted, in bytes - 2 MB.
Read buffer32768The socket read buffer, in bytes.
Wait for the acknowledgment250How long the reply is waited for, in milliseconds.
Invoke page timeout5The default timeout for test messages from the invoke screen, in seconds.
Pool size10How many connections the pool keeps open - each concurrent send uses one.
Attempts5How many times a failed send is retried.
First wait1The delay before the first retry, in seconds, doubling with each attempt.
Longest wait300The upper limit for the delay between retries, in seconds.
Jitter10The random percentage applied to each retry delay.
Failures50The failure percentage within the window that pauses sending.
Within60The rolling window the failure percentage is computed over, in seconds.
Try again after60How long sending stays paused before a trial message goes through, in seconds.
CA bundle-The bundle the server is verified against - setting it turns TLS on.
Client certificate-The certificate presented for mutual TLS.
Client key-The private key matching the client certificate.
Log each message to the server logoffMessage bodies carry patient data, so turn it on for troubleshooting and back off after.
Log levelINFOThe level this connection logs its own entries at.
Record each message in the audit logonEach message sent and each acknowledgment received goes to the audit log.

See also

PageWhat it covers
Receiving over MLLPAccept HL7 v2 messages from clinical systems
AcknowledgmentsEvery AckResult field and what each code means
The audit logEvery sent message with its acknowledgment, including outstanding ones
Enmasse referenceThe connection fields as YAML keys, for version-controlled deployments

Learn more


Schedule a meaningful demo

Book a demo with an expert who will help you build meaningful systems that match your ambitions

"We evaluated 12 integration platforms and Zato was the only one to score 100%."

Philip Zuñiga, Assistant Professor, University of the Philippines