The smallest complete print system is a few dozen lines of Node: open a TCP socket to a receipt printer, encode some ESC/POS bytes, write them, close. Wes Bos built exactly that during a Sentry hack week and talked through it with Scott Tolinski on Syntax episode 822, "Receipt Printer with JavaScript", published September 16, 2024. Every new Sentry issue came out of an 80 mm thermal printer on his desk. He puts the printer part at maybe thirty or forty lines of code.
The episode is two years old and still worth the listen, because the interesting part is not those forty lines. It is the two problems Wes hit after they worked. Both are the first appearance of a problem every production print system has to solve.
Forty lines of Node is a complete print system
Here is the shape of what he built, from the episode and the companion repo:
- The printer is an 80 mm thermal receipt printer, the kind under a restaurant counter, hooked up over wired Ethernet. The repo names a Rongta and adds that almost any printer will do, since they all speak ESC/POS. Press a series of buttons on the printer and it prints its own IP address and port.
- The transport is a raw TCP socket from Node's
netmodule. He tried WebSockets first, but the printer wants plain TCP, and a browser cannot open a TCP connection. So the Node process is the thing beside the printer. - The encoding is ESC/POS, produced by Niels Leenheer's ESC/POS encoder library. You call
.align('center').bold(true).text('...')and get a byte array to write to the socket. (That library has since been deprecated in favor of ReceiptPrinterEncoder, which is backwards compatible; use the new name.) - The layout is HTML and CSS, screenshotted with Playwright and sent as a dithered image, because ESC/POS text formatting runs out fast and a bitmap can look like anything.
- The trigger is a Sentry webhook. He registered an app in Sentry with webhooks on new and updated issues, pointed at an API route in a Next.js app, which renders the receipt, screenshots it, and writes the image to the socket.
- The extra is a toxicity classifier via Transformers.js, because the project also let strangers on the internet type text onto the printer, and Wes did not want his desk to become a content moderation problem. By his account it caught nearly everything.
That is the whole architecture, and it is the one the hobby projects that keep reaching the Hacker News front page use too. We wrote up that pattern in why developers keep falling in love with receipt printers, and the byte-level details in the ESC/POS field guide.
The four decisions that were right
These are the choices people get wrong when they start from a document-printing mental model.
Direct TCP, not a driver. A receipt printer with a network card is a socket that happens to burn paper. No driver, no print dialog, no spooler. The bytes you write are the bytes the printer executes.
Render to an image, not to commands. Wes could have spent the week learning ESC/POS text modes. Instead he designed receipts in the tool he already knew, took a screenshot, and let the encoder dither it. The ThermalMarky thread on Hacker News in March 2026 arrived at the same answer independently. Render on the computer, send a bitmap, keep the printer dumb.
A webhook as the trigger. The printer does not poll Sentry. Sentry tells the app, the app prints. That is the right direction for an event that should produce a physical object, and the same direction an order system should use for a label.
A filter in front of the printer. Paper is permanent and public. Anything that lets untrusted input reach a printer needs a gate, whether that gate is a toxicity model or an allowlist of what a caller may print. We make the same argument, more formally, in giving an AI agent a printer.
The two problems were the real lesson
The first print worked on the first try, which he notes is not how this usually goes. Then two things went wrong, and he describes both clearly enough to read as design constraints rather than anecdotes.
One socket. The printer accepts one socket connection at a time. Wes's Next.js app tried to open two, and he traced it to Next.js putting React Server Components into two different webpack bundles, so the module holding the connection effectively existed twice. On air he compares it to hitting connection limits on a database and reaching for a pool.
That is the seed of a real production rule: a printer is a serial device with a single consumer, and everything upstream has to be arranged so exactly one thing talks to it at a time. In a hobby project that is a singleton. In a fleet it is a dispatcher that leases each job to one worker.
A flood of paper. Before the Sentry receipts, he put a webcam photo booth in front of the same printer, exposed it through a Cloudflare tunnel, and shared the link. Thousands of people sent photos. The Next.js dev server, he says, held up just fine. The desk did not: "My whole desk was full of paper." He turned the photo feature off before it spread much past his own circles.
That is the other seed, and note where it bit: the software scaled, the physical device did not. A system in front of a printer has to accept work, persist it, drain it at the printer's pace, and decide what happens to work that waits too long. A photo that prints an hour after it was sent is a curiosity. A shipping label that prints an hour after the parcel left is a mis-ship.
A hack-week printer and a warehouse printer differ in exactly three ways
Take Wes's build and change one variable: the paper now matters to someone other than the person at the desk. A kitchen ticket, a shipping label, a prescription label. Three things change, and none of them is the ESC/POS.
Identity. "The printer" becomes "which printer, at which site, for which customer." Printers get renamed and re-plugged, so the id has to be assigned by the system, not read off the device.
Durability. The job has to exist somewhere durable before anything tries to print it, and it has to have an expiry, because the machine beside the printer will be asleep, rebooting, or unplugged some fraction of the time. Wes's desk full of paper was the friendly version of this. The unfriendly version is a warehouse PC that comes back on Monday and prints Friday's queue.
Retries. With a message printer, a duplicate costs a strip of paper. With a label, a duplicate costs a parcel. So the instruction needs a deterministic identity, and both the server and the machine beside the printer need a record of what already printed. We wrote that up in how idempotency prevents duplicate shipping labels, and the "we do not know whether it printed" state in state machines for side effects you cannot fully observe.
Everything else in the hack-week build survives contact with production: the direct byte path, the render-to-image trick, and the webhook trigger.
Build it this weekend, and what to change before Monday
If the episode makes you want a printer, the shopping list is short: any 80 mm or 58 mm ESC/POS printer with Ethernet or USB, a roll of paper, and Node. The encoder library, under its new name, gives you text, alignment, QR codes, barcodes, and dithered images, and hands back bytes you write to whatever transport you have.
Before you point anything you care about at it, add two things Wes's project did not need:
- A queue with a deadline. Accept the job into a table, give it an expiry, and have one worker drain it. That single change addresses both problems from the episode: one consumer for the device, and somewhere for work to wait.
- A key per intended print. Derive it from the business object, not the request, so a retry returns the existing job instead of another receipt.
Neither is more than an hour of work, and both separate a demo from something a shop can run.
Where we come in, and where we do not
RocketPrint is a remote printing API and desktop Station for software platforms: your backend POSTs a print job, and a small app beside the printer prints it, with no inbound networking, no drivers for raw label formats, and a durable job record. The bytes we send to a receipt printer are the same ESC/POS bytes the encoder produces, base64-encoded in a raw_base64 job, and the queue, expiry, and idempotency above are what the job record gives you.
Two honest limits. Wes's printer was driven from a machine on his own desk, and for one printer that is a better design than ours: there is nothing between the app and the socket. And if that machine is a Raspberry Pi or any other Linux box, we cannot help yet; Station ships for macOS and Windows only. Where we make sense is the moment the printer is in someone else's building and there are forty of them.
Go listen to the episode. Then, if you want to know what the bytes were doing, the ESC/POS field guide and the printers and formats page pick up where it leaves off.
Episode details verified September 11, 2026.
END / syntax-fm-receipt-printer-javascript-sentry-alerts
More field notes →