RTFM · Medical healthchecks
Medical checks: DICOM, HL7 and FHIR, explained
Hospitals run on three languages: DICOM moves the images, HL7 moves the messages, FHIR moves the records. Saphira's medical check package health-checks all three; with synthetic data only, and hard guard rails on anything that could change a real patient record.
Three languages, in plain words
When a radiographer presses the button on a CT scanner, the images travel to the hospital's archive (the PACS) speaking DICOM. When the booking system tells the radiology system that a patient has arrived, that message speaks HL7. When a modern app asks for a patient's allergies or medications, it speaks FHIR. A load balancer sitting in front of these systems must check them in their own languages; an HTTP port check would happily declare a PACS "healthy" while its DICOM door is jammed.
| Language | Carries | Saphira checks |
|---|---|---|
| DICOM (classic, TCP 104) | Images and imaging work: echo, find, move, store | dicom-echo · dicom-find · dicom-move · dicom-store |
| DICOMweb (HTTP) | The same work over REST: query (QIDO), retrieve (WADO), store (STOW) | dicom-qido · dicom-wado · dicom-stow |
| HL7 v2 over MLLP | Admission, discharge, orders; the message bus of the hospital | hl7-mllp |
| FHIR (HTTP) | Structured clinical records for modern integrations | fhir |
How a DICOM check talks to a scanner or PACS
Every DICOM conversation has two ends with names called AE titles: the SCU (service class user, the asker) and the SCP (service class provider, the answerer). The checks use the DCMTK toolkit's SCU tools (echoscu, findscu, movescu, storescu) to play the asking role, and every knob they need, which AE title to call as, which to call, which patient keys to query; is configuration in the same LB_SAPHIRA_* layer as the generic checks:
# $6/$7 convenience positionals or env or conf file:
LB_SAPHIRA_DICOM_AET=SAPHIRA-HC # who we are (calling AE)
LB_SAPHIRA_DICOM_AEC=MODALITY-SCP # who we call (called AE)
LB_SAPHIRA_DICOM_PATIENT_ID=SAPHIRA-SYNTHETIC-HEALTHCHECK
LB_SAPHIRA_DICOM_MODALITY=CT # only sent when configured
LB_SAPHIRA_DICOM_QUERY_LEVEL=PATIENT # PATIENT|STUDY
This matters for modality load balancing specifically: a CT suite from vendor A may require a specific called AE title and query level, and the per-service conf file (dicom.find/<service>.conf, service token = sanitized $1) lets each VIP carry its own. Neither HAProxy nor ldirectord needs a new interface for it; the configuration arrives through the same $1–$5 ABI and file layer as every other check.
Safety classes: cheap vs deep
Reading is cheap; writing changes the world. The medical checks are split by what they do to the systems they touch:
| Class | Behaviour | Checks |
|---|---|---|
| Cheap / continuous-safe | Read-only synthetic transactions; safe at any check interval | dicom-echo · dicom-find · dicom-qido · dicom-wado · hl7-mllp · fhir |
| Deep / opt-in (guard-gated) | State-changing: they store objects or trigger retrieves. Hard-refuse with exit 2 unless explicitly enabled | dicom-move · dicom-store · dicom-stow |
# The guard: every state-changing check requires BOTH
LB_SAPHIRA_STATE_CHANGING=1
# AND its dedicated target (e.g. LB_SAPHIRA_DICOM_MOVE_DEST
# or LB_SAPHIRA_DICOM_STOW_URL). Missing either → exit 2.
Never enable the deep checks as high-frequency defaults. They exist to prove the full write path before a go-live or after maintenance; run them on demand or at long intervals against a dedicated health-check endpoint, never against a production archive at 5-second cadence.
The C-MOVE rule
A C-MOVE asks the PACS to send images somewhere else; the 'move destination' SCP. Running movescu alone proves nothing: the move instruction can succeed while the actual image transfer silently fails. The dicom-move check therefore requires a dedicated receiving SCP (for example a health-check storescp instance) configured as the move destination via LB_SAPHIRA_DICOM_MOVE_DEST, and fails closed without one. The move is only 'healthy' when the retrieve pipeline genuinely runs.
Synthetic data only: the privacy guarantee
No real patient ever appears in a Saphira health check. Query keys are synthetic (patient ID SAPHIRA-SYNTHETIC-HEALTHCHECK, a non-existent HL7 patient SAPHIRA-SYNTHETIC-PATIENT); stored objects are generated at check time by the packaged lb-saphira-dicom-synth.py; a minimal valid 2×2-pixel Secondary Capture whose PatientName is SAPHIRA^SYNTHETIC^HEALTHCHECK and whose UIDs are synthetic. No upstream sample files ship, so nothing resembling real patient data can leak into your estate through the health checks.
The HL7 MLLP check additionally asserts the ACK's MSA-1 field is AA or CA (accepted/committed-accept); a valid application-level acknowledgement, not merely a socket that answered. LB_SAPHIRA_HL7_MESSAGE can override the payload entirely, for sites whose HIS needs a specific query shape.
The nine checks
-
1. Continuous-safe first
dicom-echo proves the association; dicom-find proves query; dicom-qido/wado prove the DICOMweb read paths; hl7-mllp and fhir prove the message and record planes.
-
2. Add the deep checks deliberately
dicom-move, dicom-store, dicom-stow with the guard enabled, against dedicated targets, to certify the write path.
-
3. Balance on the cheap verdicts
Let the balancers use continuous-safe checks for day-to-day routing decisions; schedule the deep ones for go-live proof.
Did we miss something?
If this page left something unanswered, found an error, or there is another subject you would like documented, tell us. Saphira’s documentation grows from real problems people need to solve.
Send feedback or request a new section →
Prefer not to do it yourself?
Everything needed to do the work yourself is documented here and remains free; we charge for human time, not for withholding knowledge. Sometimes the missing resource is simply time. The same people who build Saphira can provide paid professional help with implementation, migration, troubleshooting and administration.