I Poked a 10-Year-Old Chat Protocol With a Stick Developer Maneshwar spent a weekend exploring lhttp, a decade-old chat protocol built on websockets that mimics HTTP syntax. Despite being written for Go 1.6 and using outdated dependency management, the project compiled and ran on Go 1.24 without any modifications. The protocol uses NATS for server-to-server communication and features a simple text-based wire format. Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback. So here's how I spent my weekend. I went digging through old GitHub repos looking for something forgotten, found a project called lhttp , and instead of just reading the README like a reasonable person, I cloned it, compiled it, ran it, and poked it with a stick until it did something weird. It did something weird. We'll get there. lhttp https://github.com/fanux/lhttp describes itself as "a http like protocol using websocket to provide long live, build your IM service quickly scalable without XMPP." Translation: it's a tiny text protocol, styled like HTTP, that rides on top of a websocket connection instead of a raw TCP socket. The pitch is that you get HTTP's readability commands, headers, a body but with a connection that stays open, so you can push messages to clients instead of waiting for them to ask nicely. Under the hood, when you want to run more than one lhttp server because one server is never enough once your chat app takes off, right? , all the servers talk to each other through NATS https://nats.io , specifically an ancient bundled copy of gnatsd . Every server subscribes to the same message bus, so it doesn't matter which server a client is connected to. Publish a message on server 1, and a client sitting on server 3 still gets it. Here's the cluster idea, minus the ASCII art from the README: Nice idea, honestly. Small gnat-sized message broker, quietly doing the heavy lifting so the actual servers can stay dumb and stateless. Naming a broker after an insect that's small but annoyingly persistent feels like accidental self-awareness. The wire protocol itself is charmingly blunt: LHTTP/1.0 chat content-type:json publish:channel jack {"to":"jack","message":"hello jack"} Command line, headers, blank line, body. If you've ever hand-rolled an HTTP request in telnet for fun, this will feel instantly familiar. Here's where I expected pain. This repo uses Godeps , a vendor/ folder frozen in time, and a Godeps.json that proudly declares "GoVersion": "go1.6" . For context, Go modules didn't exist yet in 2016. People vendored dependencies by literally copying the source into your repo and hoping nobody touched it again. I have Go 1.24 installed. I fully expected to spend my evening untangling import paths. export GOPATH=/some/path/gopath mkdir -p "$GOPATH/src/github.com/fanux" ln -s /path/to/lhttp "$GOPATH/src/github.com/fanux/lhttp" cd "$GOPATH/src/github.com/fanux/lhttp" GO111MODULE=off go build . Exit code zero. First try. No fights, no missing symbols, no "this API doesn't exist anymore." Old-school GOPATH mode plus a vendor folder from the Obama administration, and Go 1.24 just shrugged and compiled it. Same story for the actual demo server websocketServer/test.go , which despite the filename is a real main package and the tiny static file server in lhttp-web-demo https://github.com/fanux/lhttp-web-demo that just serves the chat UI. Both compiled clean with zero source changes. One small archaeology note for anyone following along: the main README's Docker instructions tell you to expose port 8081 for the lhttp server. The actual source code hardcodes :8581 . One digit off, in the docs, for what appears to be the project's whole life. Nobody tell them. Actually, somebody should tell them. Three processes, three jobs: ./gnatsd -p 4222 & the message bus ./lhttp server & the actual chat server, listening on :8581 ./demo server & serves index.html on :9090 With everything up, I didn't even bother with a browser first. I went straight for a raw websocket connection in Python, because I wanted to see the bytes. ws.send "LHTTP/1.0 chat\r\n\r\nHello from the blog experiment " ws.recv 'LHTTP/1.0 auth\r\ncontent-type:image/png\r\n\r\nHello from the blog experiment ' I sent plain text under the chat command. I got back a response labeled command auth , with a header claiming the content type is image/png . My text message. Labeled as a PNG. The demo's ChatProcessor apparently relabels every reply this way regardless of what you send it, which I can only assume was a debugging leftover that nobody removed before shipping the reference demo. It's the kind of bug you'd only find by actually running the thing instead of reading the README, which is exactly why I'm writing this instead of just linking you the repo. Pub/sub worked exactly as advertised, though. Two connections, one subscribes to a channel, the other publishes: client A ws.send "LHTTP/1.0 subpub\r\nsubscribe:room1\r\n\r\n" client B ws.send "LHTTP/1.0 subpub\r\npublish:room1\r\n\r\nHey room1, anyone here?" client A receives: 'LHTTP/1.0 subpub\r\npublish:room1\r\n\r\nHey room1, anyone here?' Clean fanout through NATS, no drama. Good, functioning building block. The actual chat demo the one with the Vue.js frontend and the little GIF in the README doesn't use a generic subpub command. It uses chat for everything, subscribing to a channel called chatroom the moment the page loads, then publishing to that same channel every time you hit Send. So I replicated exactly what the browser does, using two raw connections named, obviously, Jack and Mike. jack.send "LHTTP/1.0 chat\r\nsubscribe:chatroom\r\n\r\n" mike.send "LHTTP/1.0 chat\r\nsubscribe:chatroom\r\n\r\n" jack.send "LHTTP/1.0 chat\r\npublish:chatroom\r\n\r\nyo mike, lhttp works " Jack received two messages back. Mike received one. jack got: 'LHTTP/1.0 auth\r\ncontent-type:image/png\r\n\r\nyo mike, lhttp works ' 'LHTTP/1.0 chat\r\npublish:chatroom\r\n\r\nyo mike, lhttp works ' mike got: 'LHTTP/1.0 chat\r\npublish:chatroom\r\n\r\nyo mike, lhttp works ' Here's why. The chat command handler always fires off a direct personal reply the instant it receives anything that's the mislabeled "auth"/"image/png" echo from earlier . Separately, and completely independently, the pub/sub filter re-publishes your message to the NATS channel you're posting to. Since you subscribed to your own chatroom when you opened the connection, you're also a subscriber of the very channel you just published to. Nobody else in the room sees the duplicate. Only the sender does. Which means if you were building on this today without checking, you'd get bug reports along the lines of "why does my own message show up twice but nobody else's does," and the answer would sound completely made up until you draw this exact diagram. The README claims 10,000 messages published in 0.04 seconds on a single core with 1GB of RAM, which is a wildly specific benchmark from 2016 that I have no way to reproduce on identical hardware anymore. Instead of just repeating someone else's decade-old number, I ran my own, on whatever this machine happens to be: for i in range 10000 : ws.send f"LHTTP/1.0 chat\r\n\r\nmsg-{i}" for i in range 10000 : ws.recv 10,000 request/response round trips over a single websocket, from a single-threaded Python client, no batching tricks: 0.558 seconds, about 17,900 messages per second. Not the same benchmark, not the same hardware, not really a fair comparison at all, but it does confirm the core loop isn't doing anything silly like blocking on disk or sleeping between messages. For a text parser held together with string indexing buildMessage in wsHandler.go walks the raw string character by character, no regex, no fancy tokenizer , that's respectable. Honestly? Probably not for anything new. golang.org/x/net/websocket what lhttp is built on has been the "please use gorilla/websocket or nhooyr.io/websocket instead" example in Go circles for years now, and the protocol's own reference demo has a bug you can find in about ten minutes of poking. But as a piece of "here's how people solved real-time messaging before everyone standardized on Socket.IO or plain WebSocket JSON frames," it's a genuinely fun read, and it's kind of wonderful that a 2016 vendor folder can still be handed to a 2026 Go compiler and just work without complaint. If you want to try breaking it yourself, the two repos are linked above. Bring your own Jack and Mike. AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production. git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free. Any feedback or contributors are welcome It's online, source-available, and ready for anyone to use. ⭐ Star it on GitHub: | 🇩🇰 Dansk https://github.com/HexmosTech/git-lrc/readme/README.da.md | 🇪🇸 Español https://github.com/HexmosTech/git-lrc/readme/README.es.md | 🇮🇷 Farsi https://github.com/HexmosTech/git-lrc/readme/README.fa.md | 🇫🇮 Suomi https://github.com/HexmosTech/git-lrc/readme/README.fi.md | 🇯🇵 日本語 https://github.com/HexmosTech/git-lrc/readme/README.ja.md | 🇳🇴 Norsk https://github.com/HexmosTech/git-lrc/readme/README.nn.md | 🇵🇹 Português https://github.com/HexmosTech/git-lrc/readme/README.pt.md | 🇷🇺 Русский https://github.com/HexmosTech/git-lrc/readme/README.ru.md | 🇦🇱 Shqip https://github.com/HexmosTech/git-lrc/readme/README.sq.md | 🇨🇳 中文 https://github.com/HexmosTech/git-lrc/readme/README.zh.md | 🇮🇳 हिन्दी https://github.com/HexmosTech/git-lrc/readme/README.hi.md | GenAI today is a race car without brakes . It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents silently break things : they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production. git-lrc is your braking system. It hooks into git commit and runs an AI review on every diff In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen At a glance: 10 risk categories https://github.com/HexmosTech/git-lrc what-git-lrc-checks-for · 100+ failure patterns tracked https://github.com/HexmosTech/git-lrc what-git-lrc-checks-for · every commit…