Skip to content
Free shipping on orders over $2,000.

The print bridge

How rendered ESC/POS receipts reach a printer on the customer's own network: the claim-then-acknowledge loop, the claim timeout, and the two silent failure modes to watch for.

7 min read

Your server never talks to a printer. It hands the device a rendered receipt and a LAN address, and the device opens the socket from inside the customer's own network — which is what makes printing work through NAT with no VPN, no port forwarding and no static IP at the site. Render ESC/POS server-side and a receipt layout change never needs a firmware update.

The loop

httpGET /restaurant/print-jobs
GET /restaurant/print-jobs HTTP/1.1
x-device-id: AA:BB:CC:DD:EE:FF

──────────────────────────────────────────────
204 No Content        → nothing to print. Poll again in 3 seconds.

200 OK
{
  "order_id": 344,
  "printer_host": "192.168.18.87",
  "printer_port": 9100,
  "escpos_b64": "G0AbYQEuLi4="
}

Base64-decode escpos_b64, open a TCP socket to printer_host and printer_port, write the bytes, close. Then acknowledge.

httpPOST /restaurant/orders/{id}/printed
POST /restaurant/orders/344/printed HTTP/1.1
x-device-id: AA:BB:CC:DD:EE:FF

──────────────────────────────────────────────
{ "order_id": 344, "printed_at": "2026-09-10T10:22:31Z", "already": false }

Claim mechanics

  • Claiming is an atomic compare-and-swap, so several devices polling the same queue in the same millisecond cannot both win a ticket.
  • A claimed-but-unacknowledged ticket is released to other devices after 90 seconds — long enough for a print and a few polls, short enough that a device that died mid-job does not strand the kitchen.
  • The same device may re-claim its own unacknowledged ticket immediately. If you rebooted mid-job nobody else can print it anyway, so retrying at once beats making the kitchen wait out the timeout.
  • The acknowledgement is idempotent — the first one wins. A device that prints, acknowledges, loses the response and acknowledges again does not corrupt the recorded print time.

A complete print bridge

pythonprint_bridge.py — poll, print, acknowledge
import base64, socket, time, requests

HOST = "https://api.yourcompany.com"
MAC  = "AA:BB:CC:DD:EE:FF"

def print_once():
    r = requests.get(f"{HOST}/restaurant/print-jobs",
                     headers={"x-device-id": MAC}, timeout=10)
    if r.status_code == 204:
        return False                       # empty queue — the normal case
    r.raise_for_status()
    job = r.json()

    payload = base64.b64decode(job["escpos_b64"])
    with socket.create_connection(
            (job["printer_host"], job["printer_port"]), timeout=5) as sock:
        sock.sendall(payload)

    # Acknowledge, always. Skipping this reprints this ticket forever and
    # blocks every newer order behind it.
    ack = requests.post(f"{HOST}/restaurant/orders/{job['order_id']}/printed",
                        headers={"x-device-id": MAC}, timeout=10)
    if ack.status_code == 403:
        raise RuntimeError("wrong tenant: x-device-id resolves to another property")
    ack.raise_for_status()
    return True

while True:
    try:
        if not print_once():
            time.sleep(3)                  # the claim timeout assumes ~3 s polling
    except OSError as exc:
        # Printer unreachable. Do NOT acknowledge — let the 90 s claim timeout
        # return the ticket to the queue so another device can take it.
        print("printer unreachable:", exc)
        time.sleep(3)

Two silent failure modes to design against

Printer not configured

If the property has no printer_host, the backend releases the claim and answers 204 — identical to an empty queue. Orders pile up and nothing prints, with no error anywhere. This has happened in production: four orders sat unprinted while a correctly configured device polled every few seconds.

bashThe watchdog that catches it
curl -s "https://api.yourcompany.com/restaurant/print-health?property_id=4" \
  -H "Authorization: Bearer $TOKEN"

# → { "property_id": 4, "unprinted": 0,
#     "oldest_age_seconds": null, "stale": false }

Wrong tenant

Acknowledging an order that belongs to another property returns 403. If you see those, your x-device-id is resolving to a device on the wrong property — a provisioning fault, not a transient one.

Something wrong or missing on this page? Tell us.