# I Poked a 10-Year-Old Chat Protocol With a Stick

> Source: <https://dev.to/lovestaco/i-poked-a-10-year-old-chat-protocol-with-a-stick-2g4h>
> Published: 2026-07-12 17:49:36+00:00

*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…
