{"slug": "i-poked-a-10-year-old-chat-protocol-with-a-stick", "title": "I Poked a 10-Year-Old Chat Protocol With a Stick", "summary": "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.", "body_md": "*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.*\n\nSo here's how I spent my weekend.\n\nI 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.\n\nIt did something weird. We'll get there.\n\n[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.\"\n\nTranslation: it's a tiny text protocol, styled like HTTP, that rides on top of a websocket connection instead of a raw TCP socket.\n\nThe 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.\n\nUnder 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`\n\n.\n\nEvery server subscribes to the same message bus, so it doesn't matter which server a client is connected to.\n\nPublish a message on server 1, and a client sitting on server 3 still gets it.\n\nHere's the cluster idea, minus the ASCII art from the README:\n\nNice idea, honestly. Small gnat-sized message broker, quietly doing the heavy lifting so the actual servers can stay dumb and stateless.\n\nNaming a broker after an insect that's small but annoyingly persistent feels like accidental self-awareness.\n\nThe wire protocol itself is charmingly blunt:\n\n```\nLHTTP/1.0 chat\ncontent-type:json\npublish:channel_jack\n\n{\"to\":\"jack\",\"message\":\"hello jack\"}\n```\n\nCommand line, headers, blank line, body.\n\nIf you've ever hand-rolled an HTTP request in telnet for fun, this will feel instantly familiar.\n\nHere's where I expected pain.\n\nThis repo uses `Godeps`\n\n, a `vendor/`\n\nfolder frozen in time, and a `Godeps.json`\n\nthat proudly declares `\"GoVersion\": \"go1.6\"`\n\n.\n\nFor context, Go modules didn't exist yet in 2016.\n\nPeople vendored dependencies by literally copying the source into your repo and hoping nobody touched it again.\n\nI have Go 1.24 installed. I fully expected to spend my evening untangling import paths.\n\n```\nexport GOPATH=/some/path/gopath\nmkdir -p \"$GOPATH/src/github.com/fanux\"\nln -s /path/to/lhttp \"$GOPATH/src/github.com/fanux/lhttp\"\ncd \"$GOPATH/src/github.com/fanux/lhttp\"\nGO111MODULE=off go build .\n```\n\nExit code zero. First try. No fights, no missing symbols, no \"this API doesn't exist anymore.\"\n\nOld-school GOPATH mode plus a vendor folder from the Obama administration, and Go 1.24 just shrugged and compiled it.\n\nSame story for the actual demo server (`websocketServer/test.go`\n\n, which despite the filename is a real `main`\n\npackage) 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.\n\nOne small archaeology note for anyone following along: the main README's Docker instructions tell you to expose port 8081 for the lhttp server.\n\nThe actual source code hardcodes `:8581`\n\n.\n\nOne digit off, in the docs, for what appears to be the project's whole life. Nobody tell them. Actually, somebody should tell them.\n\nThree processes, three jobs:\n\n```\n./gnatsd -p 4222 &          # the message bus\n./lhttp_server &             # the actual chat server, listening on :8581\n./demo_server &               # serves index.html on :9090\n```\n\nWith everything up, I didn't even bother with a browser first.\n\nI went straight for a raw websocket connection in Python, because I wanted to see the bytes.\n\n```\nws.send(\"LHTTP/1.0 chat\\r\\n\\r\\nHello from the blog experiment!\")\nws.recv()\n# 'LHTTP/1.0 auth\\r\\ncontent-type:image/png\\r\\n\\r\\nHello from the blog experiment!'\n```\n\nI sent plain text under the `chat`\n\ncommand.\n\nI got back a response labeled command `auth`\n\n, with a header claiming the content type is `image/png`\n\n. My text message. Labeled as a PNG.\n\nThe demo's `ChatProcessor`\n\napparently 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.\n\nIt'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.\n\nPub/sub worked exactly as advertised, though. Two connections, one subscribes to a channel, the other publishes:\n\n```\n# client A\nws.send(\"LHTTP/1.0 subpub\\r\\nsubscribe:room1\\r\\n\\r\\n\")\n# client B\nws.send(\"LHTTP/1.0 subpub\\r\\npublish:room1\\r\\n\\r\\nHey room1, anyone here?\")\n# client A receives:\n# 'LHTTP/1.0 subpub\\r\\npublish:room1\\r\\n\\r\\nHey room1, anyone here?'\n```\n\nClean fanout through NATS, no drama. Good, functioning building block.\n\nThe actual chat demo (the one with the Vue.js frontend and the little GIF in the README) doesn't use a generic `subpub`\n\ncommand.\n\nIt uses `chat`\n\nfor everything, subscribing to a channel called `chatroom`\n\nthe moment the page loads, then publishing to that same channel every time you hit Send.\n\nSo I replicated exactly what the browser does, using two raw connections named, obviously, Jack and Mike.\n\n```\njack.send(\"LHTTP/1.0 chat\\r\\nsubscribe:chatroom\\r\\n\\r\\n\")\nmike.send(\"LHTTP/1.0 chat\\r\\nsubscribe:chatroom\\r\\n\\r\\n\")\n\njack.send(\"LHTTP/1.0 chat\\r\\npublish:chatroom\\r\\n\\r\\nyo mike, lhttp works!\")\n```\n\nJack received two messages back. Mike received one.\n\n```\njack got:\n  'LHTTP/1.0 auth\\r\\ncontent-type:image/png\\r\\n\\r\\nyo mike, lhttp works!'\n  'LHTTP/1.0 chat\\r\\npublish:chatroom\\r\\n\\r\\nyo mike, lhttp works!'\n\nmike got:\n  'LHTTP/1.0 chat\\r\\npublish:chatroom\\r\\n\\r\\nyo mike, lhttp works!'\n```\n\nHere's why. The `chat`\n\ncommand handler always fires off a direct personal reply the instant it receives anything (that's the mislabeled \"auth\"/\"image/png\" echo from earlier).\n\nSeparately, and completely independently, the pub/sub filter re-publishes your message to the NATS channel you're posting to.\n\nSince you subscribed to your own chatroom when you opened the connection, you're also a subscriber of the very channel you just published to.\n\nNobody else in the room sees the duplicate. Only the sender does.\n\nWhich 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.\n\nThe 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.\n\nInstead of just repeating someone else's decade-old number, I ran my own, on whatever this machine happens to be:\n\n```\nfor i in range(10000):\n    ws.send(f\"LHTTP/1.0 chat\\r\\n\\r\\nmsg-{i}\")\nfor i in range(10000):\n    ws.recv()\n```\n\n10,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.**\n\nNot 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.\n\nFor a text parser held together with string indexing (`buildMessage`\n\nin `wsHandler.go`\n\nwalks the raw string character by character, no regex, no fancy tokenizer), that's respectable.\n\nHonestly? Probably not for anything new.\n\n`golang.org/x/net/websocket`\n\n(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.\n\nBut 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.\n\nIf you want to try breaking it yourself, the two repos are linked above. Bring your own Jack and Mike.\n\nAI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.\n\ngit-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.\n\nAny feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.\n\n⭐ Star it on GitHub:\n\n| [🇩🇰 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) |\n\nGenAI 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.\n\n** git-lrc is your braking system.** It hooks into\n\n`git commit`\n\nand runs an AI review on every diff In short, git-lrc helps **Prevent Outages, Breaches, and Technical Debt Before They Happen**\n\n**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…", "url": "https://wpnews.pro/news/i-poked-a-10-year-old-chat-protocol-with-a-stick", "canonical_source": "https://dev.to/lovestaco/i-poked-a-10-year-old-chat-protocol-with-a-stick-2g4h", "published_at": "2026-07-12 17:49:36+00:00", "updated_at": "2026-07-12 18:14:41.378148+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Maneshwar", "lhttp", "NATS", "gnatsd", "GitHub", "Go"], "alternates": {"html": "https://wpnews.pro/news/i-poked-a-10-year-old-chat-protocol-with-a-stick", "markdown": "https://wpnews.pro/news/i-poked-a-10-year-old-chat-protocol-with-a-stick.md", "text": "https://wpnews.pro/news/i-poked-a-10-year-old-chat-protocol-with-a-stick.txt", "jsonld": "https://wpnews.pro/news/i-poked-a-10-year-old-chat-protocol-with-a-stick.jsonld"}}