# roomer: A Room-Based WebSocket Framework with Formal Specs and Cluster Adapters

> roomer is a Go and JavaScript framework for room-based WebSocket messaging, featuring zero-copy binary framing, pluggable cluster adapters, and a TLA+ specification. This review examines its architecture, setup, and limits for engineers considering adoption.

**joncody/roomer** — Concurrent, room-based WebSocket messaging server and pub/sub hub written in Go.

- Repository: https://github.com/joncody/roomer
- Stars: 28 · Forks: 5
- Language: JavaScript
- License: MIT
- Published: 2026-08-27 · Updated: 2026-08-27 · Language: en
- Canonical page: https://hysenlabs.com/projects/joncody-roomer

## What roomer Solves and Who It Targets

roomer addresses a specific problem: building real-time applications where clients need to join and leave named rooms over a single WebSocket connection. Many WebSocket libraries force you to manage room membership and message routing yourself. roomer packages that logic into a Go server and a JavaScript client. The target user is a Go backend developer who wants a lightweight, self-contained solution without pulling in a heavy framework. The README emphasizes zero client dependencies and zero-copy binary framing, which appeals to teams with latency-sensitive chat, collaboration, or live data feeds. The inclusion of a TLA+ specification suggests a focus on correctness, which is unusual for a WebSocket library. This is not a general-purpose message broker; it is a room-based hub with explicit routing.

## Architecture: Rooms, Binary Framing, and Lock-Striped Shards

The core design centers on a hub that manages connections and rooms. Each client starts in a root room and dynamically joins others. Messages are routed by room name and optional destination client ID. The protocol uses length-prefixed fields with direct slice decoding and single-allocation packet serialization. This zero-copy approach aims to reduce allocations and latency. To handle concurrency, the hub partitions connections and rooms across 32 lock-striped shards using FNV-1a hashing. This avoids a single global mutex and reduces CPU core contention. The README claims this eliminates bottlenecks, but the actual performance depends on workload. Room fanout is on-demand and non-blocking, with no per-room background goroutines. That means a slow client can potentially block the send path, though the TrySend method used in the handler example suggests a non-blocking attempt.

## Getting Started: Go Server and JavaScript Client in Practice

Installation is straightforward. For the Go server, run go get github.com/joncody/roomer. You need Go 1.21 or later. The JavaScript client is not a package; you include standalone files from src/: roomer.js, bytecursor.js, and emitter.js. The quick start shows importing roomer.js and connecting to a WebSocket URL. On the server, you register a handler for a custom event. The example registers a ping handler that replies with a pong message. Then you mount the WebSocket handler on an HTTP route. The client joins a room with root.join("lobby") and can send events with lobby.send("ping", new Uint8Array(0)). The client API includes methods like join, leave, members, and forceClose. The setup is simple, but the binary protocol means you need to understand the byte format if you write a custom client.

## Horizontal Scaling with the Redis Adapter and Custom Adapters

For multi-node deployment, roomer provides a pluggable adapter interface. The default is an in-memory adapter for single-node mode. To scale, you can use the built-in Redis adapter from adapter/redis. The example shows creating a Redis client and passing it to redisadapter.New with options like WithPrefix and WithPublishTimeout. The adapter handles loopback suppression, so a node does not receive its own published messages, preventing duplicates. The README also includes a Docker-based local cluster test: start Redis, then run two instances of examples/main.go on different ports. Messages in the lobby synchronize across nodes. The adapter interface is small: Publish, Subscribe, and Close. This makes it possible to write custom adapters for NATS, Kafka, or Postgres, though only Redis is provided. The interface is simple, but implementing a robust adapter for another system requires understanding the message format and delivery guarantees.

## Observability and Graceful Shutdown: What the README Promises

Production concerns are addressed with metrics and structured logging. The Metrics feature provides telemetry hooks for Prometheus and OpenTelemetry, covering connections, room counts, byte throughput, and drop events. Logging uses Go's log/slog, which is standard in Go 1.21+. Graceful shutdown is a package-level Shutdown(ctx) that flushes queues, sends WebSocket 1001 Going Away close frames, and cleans up connections within a deadline. This is a concrete feature that many WebSocket libraries lack. However, the README does not show how to configure the metrics hooks or how to expose them to Prometheus. You would need to read the source or documentation to wire that up. The graceful shutdown is promising, but the actual behavior under load is not described.

## Formal Verification with TLA+: A Rare but Limited Asset

One distinctive feature is the TLA+ specification in spec/roomer.tla. The README claims it proves state safety and room membership invariants. Formal verification is rare in WebSocket frameworks and suggests careful design. However, a TLA+ spec is not the same as a verified implementation. It models the protocol and invariants, but the Go code could still have bugs. The spec is a tool for reasoning, not a guarantee. For teams that value formal methods, this is a strong signal. For others, it adds little operational value. The README does not explain how to run or check the spec, so you would need TLA+ tools to use it. This is a niche feature that will appeal to a small audience.

## Limitations and Failure Modes to Consider

The project is young, with a single v1.0.0 release. The README is truncated in the provided material, so the full server API is not visible. That means you cannot evaluate all functions without cloning the repo. The JavaScript client is dependency-free, but that also means you do not get features like automatic reconnection or built-in backoff. The client API includes forceClose, but no mention of reconnection logic. The binary protocol is efficient but harder to debug than JSON. If a client or server sends a malformed frame, you need to handle it. The README mentions drop events in metrics, but does not explain what causes drops. A slow consumer could lead to message loss, which may be acceptable for some use cases but not for reliable messaging. The Redis adapter uses pub/sub, which is at-most-once delivery; if Redis connection drops, messages may be lost.

## Alternatives: How roomer Differs from Centrifugo and Socket.IO

A common alternative is Centrifugo, a Go-based real-time messaging server that also supports rooms and WebSockets. Centrifugo is a standalone server with a built-in HTTP API, while roomer is a library you embed in your Go application. That is a fundamental difference: with roomer, you write your own HTTP handler and integrate the hub into your process. Centrifugo runs as a separate service and communicates via protocols like GRPC or HTTP. Another alternative is Socket.IO, which provides rooms and automatic reconnection but uses JSON and a different protocol. Socket.IO is more mature and has a larger ecosystem, but it is not zero-copy and requires more dependencies. roomer's binary framing and TLA+ spec are differentiators, but Centrifugo offers production features like presence and history out of the box. The choice depends on whether you want a library or a service.

## Conclusion

Adopt roomer if you need a room-based WebSocket server in Go with a zero-dependency JavaScript client, especially when you want formal verification via TLA+ and the option to scale with Redis. Do not use it if you need a mature ecosystem, extensive documentation, or if your team avoids Go 1.21+ or lacks comfort with binary protocols. Before adopting, verify the current state of the repository, check the full README for the truncated server API details, and run the provided examples to confirm the clustering behavior matches your latency requirements. The single release and sparse history mean you should test thoroughly in your own environment.

## FAQ

### Which languages does roomer have server implementations for?

Three: Go, Rust and Node.js. The Go build targets Go 1.26 or later, the Rust build targets Rust 1.88 on Edition 2024 with Axum 0.8 and Tokio, and the Node build targets Node 22 or later.

### How large is the roomer wire protocol header?

12 bytes of base overhead. A 2-byte header carries a 1-byte version and a 1-byte flags value, followed by right-sized big-endian length prefixes, with room and event as uint16, destination and source as uint8, and the payload as uint32.

### Which WebSocket close codes stop the roomer client from reconnecting?

1000 Normal Closure, 1008 Policy Violation and the 4000 to 4999 application and auth range all halt the reconnection loop. A 1006 network disruption is treated as recoverable instead and retries with backoff and jitter.

### How does roomer track presence across cluster nodes?

With distributed Redis sets, using SADD, SREM and SMEMBERS, plus key-level TTL expiration applied on AddPresence. That expiry is what prunes rooms abandoned when a server crashes, without a separate heartbeat or cleanup pass.

### What backpressure policies does roomer support?

Three: DropSlowClient, which is the default and protects memory, DropOldest, which evicts from the queue, and DropNewest. Maximum message size is validated before any frame body is allocated or buffered.

### How do I run a standalone roomer server?

For Go, go run ./server/go/examples/main.go. For Rust, cargo run --manifest-path server/rust/Cargo.toml --example server, adding --features redis-adapter for clustering. For Node, cd server/node && npm start. All three serve on port 8080 by default, overridable with the PORT environment variable.

## Sources

- [Official README](https://github.com/joncody/roomer#readme)
- [Project repository](https://github.com/joncody/roomer)
- [Release notes](https://github.com/joncody/roomer/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/joncody-roomer
