7 min readJust now
–
TL;DR: I built a desk gadget that streams Hacker News comments to a cheap ESP32 display using UDP instead of HTTP/TLS. It works great, costs almost nothing to run, and proves that sometimes simple is more fun.
Press enter or click to view image in full size
Hacker News on a CYD. Talking emacs and LISP, of course. I did not scroll that wall of text.
As a whole developers have been conditioned to think that HTTP/TLS is the only “proper” way to build networked applications. But what if I told you there’s a simpler, cheaper, and more fun alternative for certain use cases?
I recently built a real-time Hacker News comment viewer that runs on a $15 ESP32 display. Instead of wrestling with TLS certificates, JSON parsing libraries, and HTTP client overhead, …
7 min readJust now
–
TL;DR: I built a desk gadget that streams Hacker News comments to a cheap ESP32 display using UDP instead of HTTP/TLS. It works great, costs almost nothing to run, and proves that sometimes simple is more fun.
Press enter or click to view image in full size
Hacker News on a CYD. Talking emacs and LISP, of course. I did not scroll that wall of text.
As a whole developers have been conditioned to think that HTTP/TLS is the only “proper” way to build networked applications. But what if I told you there’s a simpler, cheaper, and more fun alternative for certain use cases?
I recently built a real-time Hacker News comment viewer that runs on a $15 ESP32 display. Instead of wrestling with TLS certificates, JSON parsing libraries, and HTTP client overhead, I used UDP tunneled through WireGuard. The entire protocol fits in a handful of bytes, and the serverless backend costs nearly nothing to run.
It’s a Cheap Yellow Display
The ESP32-based “Cheap Yellow Display” (CYD) has become a favorite in the maker community. For about $15 on AliExpress, you get:
- ESP32 dual-core processor with WiFi
- 2.8" color touchscreen (320x240)
- Resistive touch controller
- Ambient light sensor (LDR)
- A few exposed general-purpose input/output (GPIO) pins
It’s a perfect little toy for desk widgets, sensor displays, and IoT projects. The hardware is surprisingly capable and more than enough to render HN comments in real-time.
The Problem with HTTP on IoT Devices
Most IoT tutorials will tell you to use an HTTP client library to fetch data from APIs. For Hacker News, that means:
- Establish TCP connection (handshake overhead)
- Perform TLS handshake (certificate validation, crypto negotiation)
- Send HTTP request with headers
- Parse HTTP response with headers
- Extract JSON body
- Parse JSON (allocate memory for nested structures)
- Close connection (or manage connection pooling)
For a simple “give me the next comment” request, this is a little over-the-top, so to speak. Kilobytes of code, certificate management, memory allocation for JSON parsing, and dealing with connection timeouts. And that doesn’t even get into the power consumption (a topic for a different day).
The ESP32 can handle all of the overhead of REST over TLS, but it’s a lot of moving parts for something so conceptually simple.
Enter UDP: Protocol Design Isn’t Always Hard
The “U” is for “User” not “Unreliable”. UDP gets a bad rap because it doesn’t guarantee delivery at the transport layer. And every message is important, right? Critical even? No.
The lack of a delivery guarantee doesn’t mean UDP-based protocols can’t be reliable, it just means reliability is a choice, not a mandate.
Consider this use case:
- I want the latest HN comment (one at a time)
- The client tracks the last item ID it received
- Each request includes that ID, so the server knows what to send next
- If a packet is lost (request or response), the next poll picks up where we left off
- No comments are skipped, no data is lost
This is “eventually reliable” without any TCP overhead. The protocol achieves reliability through trivial state management, not through retransmission timers and congestion control.
The Protocol
My entire protocol specification is:
Request (4 bytes):
[last_item_id: uint32]
Response (~100–800 bytes):
[item_id: uint32][author_len: uint8][author: string][0x00][text: string]
That’s it. No headers, no JSON, no TLS. The entire exchange fits in a single MTU-sized packet in each direction. And, yes, the length and null byte are redundant.
But, but… PRIVACY!
This is where WireGuard enters the picture.
Instead of bolting TLS onto every application protocol (HTTP, MQTT, CoAP, etc.), what if we secure the network layer once and then use simple protocols on top?
WireGuard is:
- Simple — Written in ~4,000 lines of code (OpenVPN is 100,000+)
- Fast — Single round-trip Handshakes, designed to be efficient in software
- Secure — Uses modern crypto (ChaCha20, Poly1305, Curve25519)
- Built-in — Kernel module on Linux, library for ESP32
On the CYD, the ESP32 establishes a WireGuard tunnel on boot, and then all UDP traffic goes through it. The VPN handles:
- Encryption
- Authentication
- NAT traversal
- Private IP addressing
Now I can use dumb… eh, simple protocols with confidence. This use case is so simple I don’t need routing on the backend at all — unless a packet is heading to port 123 (NTP) I handle it as a “next comment” request.
The Serverless Backend
The backend for the display is an AWS Lambda function written in C# (.NET 10 with Native AoT compilation). It’s triggered by UDP packets via Proxylity UDP Gateway, a service that lets Lambda functions handle UDP traffic.
Here’s the flow:
ESP32 → WireGuard → Proxylity → Lambda → HN Firebase API
The Lambda:
- Receives a UDP packet with the last item ID
- Fetches the next item from
https://hacker-news.firebaseio.com/v0/item/{id + 1}.json - Converts HTML to Markdown
- Transliterates Unicode to ASCII (for display compatibility)
- Packs it into a binary UDP response
- Returns it through the same WireGuard tunnel
Cold start time: ~100ms
Execution time: ~60ms
Cost: Within AWS free tier, or less than $0.45 otherwise
Compare that to running an always-on HTTP server! Oh, yeah, one of those can be free on AWS, too.
Why this is Fun
This architecture brings me joy because it aligns with the use case:
- Stateless is Sublime
There is no connection state here to manage. Each UDP packet is independent, so if the Lambda crashes the next packet just spawns a execution. No cleanup, no connection pools, no lingering state.
2. No Drama Reliability
The client always sends the last ID it received. Missed packets don’t cause data loss — the next poll picks up where it left off. This is reliability without overhead.
3. Simple Parsing
No JSON parsing on the ESP32 (not much in the way of parsing at all). No memory allocation for nested objects. Just read some bytes, display some strings. The protocol is so simple I could parse it in assembly (I say having never tried).
4. Privacy without Pain
By encrypting the tunnel (WireGuard), every protocol on top is automatically secure. I don’t need to implement TLS in the application layer. I don’t need to manage certificates. The VPN handles it.
5. Cheap
The backend scales to zero when not in use. Turning off the display puts the backend to sleep (zero cost), too. No servers to patch, no infrastructure to maintain.
The Developer Experience
Writing this was fun. The kind of fun that comes from removing complexity, not adding it, and moving fast.
The ESP32 code is ~500 lines of C++, and most of that is UI rendering with LVGL. The networking code? About a dozen lines:
void send_udp() { Udp.beginPacket(remote_vpn_ip, remote_vpn_port); Udp.write((uint8_t*)&last_item_id, sizeof(last_item_id)); Udp.endPacket();}void receive_udp() { while ((c = Udp.parsePacket()) > 0) { int l = Udp.read(buffer, min(c, 1024–1)); uint32_t item_id = *(uint32_t*)buffer; char* author = buffer + 5; char* text = author + buffer[4]; lv_label_set_text_fmt(text_label, "*%s*\n%s", author, text); }}
No JSON libraries. No HTTP client abstractions. No certificate pinning. Just bytes out, bytes in, pixels on.
Backend deployment iterations? One line: sam build && sam deploy.
Fine. It’s two lines and takes a few seconds. But that’s some pretty low impedance on a backend deploy.
You might be thinking: “But UDP doesn’t guarantee delivery! Won’t you lose comments?”
No. It just works. Here’s how:
The ESP32 keeps track of the last item ID it successfully received (using `0` when first booted). Every request includes this ID. So:
- Lost request? The next poll (10 seconds later) sends the same ID again.
- Lost response? The next poll sends the same ID, server sends the same comment.
- Server crash? Next request spawns a new Lambda with no state loss.
- Duplicates? No problem, they end-up as no-ops.
- Reordered responses? Shh. Nothing to see here.
No comments are ever skipped. The protocol is eventually reliable by design.
The Right Constraints
MTU-Limited Message Size: UDP packets cap at ~1500 bytes. This aligns with the display’s capabilities, since it can only show ~800 characters anyway. The constraint matches the use case.
No Ordering Guarantees: Each packet is self-contained with an item ID. Out-of-order delivery very unlikely in this sequential polling model, but even if it happens the result is just repeating an older message and then catching up again.
Single Point of Failure: The device is indeed singular. Everything else is or can be made multi-homed since it’s stateless and serverless. If I had a fleet of these displays, that’s what I’d do.
These aren’t limitations to work around — they’re constraints that simplify the design.
Should You Use UDP?
Yes. But I’m not suggesting UDP for everything. Maybe consider it when having a little fun or when:
- State is unnecessary — Each request is independent
- Loss is acceptable — Missing data doesn’t break the experience
- Latency matters — Fresh data is better than all data
- Simplicity is valuable — Easier to implement and debug
- Resources are constrained — Embedded devices, IoT sensors, thin pipes
Examples:
- Edge Sensor data streaming
- Real-time feeds (weather, stock tickers, news)
- Game state updates
- Voice/video calls
- Monitoring/logging
- Long-range, unreliable communications
Postscript
This project started on a whim: “What if I streamed HN over UDP?”
It ended up being an enjoyable build and throwback to a simpler kind of development that brought back the joy of tinkering. It’s not complex or impressive, but it’s simple and a good fit for the use case (and I do enjoy watching the opinions fly by).
For those who want to dig deeper, this project is open source:
What you’ll need:
- ESP32–2432S028R (or similar Cheap Yellow Display, ~$15-$20)
- AWS account with Proxylity UDP Gateway subscription (Lambda free tier, Proxylity free tier)
Total cost: **~$15-$20 to start, ~$0/month to run**
Software Stack
ESP32 Firmware (PlatformIO):
- Arduino framework
- LVGL 9.3 for GUI
- WireGuard-ESP32 library
- TFT_eSPI display driver
Lambda Backend (.NET 10):
- Native AOT compilation (~10MB custom runtime binary)
- Proxylity UDP Gateway SDK
- PacketDotNet for UDP manipulation
- Html2Markdown for content conversion
- AWS SAM/CloudFormation
Performance Metrics
- Packet size: 4 bytes request, 100–800 bytes response
- Latency: ~300ms round trip (
WiFi → WireGuard → Lambda → HN API → back) - Polling interval: 10 seconds
Security Model
- Encryption: WireGuard (ChaCha20-Poly1305)
- Authentication: Pre-shared keys (Curve25519)
- Exposure: Device not reachable from internet, backend firewalled
Future Enhancements?
- Interactivity. We needs buttons.
- Battery mode? Save those electrons, they aren’t making new ones you know. Wait…
Enjoy!