

Akagitsune
赤狐
A generic realtime WebSocket gateway built in Rust.
Pure transport — it relays, it does not interpret.
What It Is
A WebSocket relay that stays out of your way
Akagitsune is a generic realtime gateway — a piece of infrastructure, not a product. It connects sockets and moves bytes. What those bytes mean is entirely your decision.
Payload-Agnostic
The gateway forwards your data without touching it. Chat messages, game states, live dashboards, IoT telemetry — it all rides the same wire. The meaning is yours; the transport is ours.
Real-Time, Always
Built on asynchronous Rust with a lock-free hot path. Every message is serialized once — not once per receiver — then delivered only to the connections subscribed to its topic.
Backpressure Over Breakage
When a slow client falls behind, Akagitsune drops its queued messages and warns it — rather than slowing down everyone else. The fast stay fast; the slow get a second chance.
Two Tasks, One Connection
Each connection runs a reader (ingest and fanout into subscriber inboxes) and a writer (flush). Bounded queues everywhere, no locks in gateway code, reference-counted message clones. Clean, predictable, debuggable.
Get it running
Performance
One million, sustained
The target is a million deliveries a second inside a 10–20 ms p99 budget. On a single topic all three traffic shapes clear it, and keep clearing it at twice the load. Spread across 100 topics the same million deliveries all arrive, but p99 lands at 27.8 ms, over budget.
Service p99 against offered load
Log scale, because the spread runs from 3 ms to 28 ms. The dashed rule is the 20 ms budget; the tinted band above it is out of spec.
- ingestmany senders, one topic
- mesheveryone talks to everyone
- fanoutfew senders, many sockets
- topicssame load, 100 topics
- exploreprobe past the goal
Ingest was not run at 2M. The hollow grey markers at 3M and 3.59M are exploratory probes, and they held too: every message delivered, no breaking point found yet. The hollow red marker at 1M is the topic-spread run, the only goal-load run over budget.
Every run, in full
Latency in milliseconds. Rows that miss the budget are dimmed. Scroll the table sideways for the rest of the columns.
| Offered load | Shape | Conns | p50 | p99 | Delivered | Against budget |
|---|---|---|---|---|---|---|
| 90,000 | topics | 1000 | 2.74 | 5.10 | 100% | within budget |
| 124,750 | churn | 500 | 5.30 | 50.30 | 100% | over budget |
| 450,000 | topics | 5000 | 46.21 | 71.42 | 100% | over budget |
| 1,000,000 | ingest | 101 | 1.99 | 4.53 | 100% | within budget |
| 1,000,000 | fanout | 501 | 1.84 | 4.72 | 100% | within budget |
| 1,000,000 | mesh | 201 | 3.10 | 5.97 | 100% | within budget |
| 1,000,000 | topics | 2100 | 18.62 | 27.84 | 100% | over budget |
| 1,500,000 | ingest | 101 | 1.58 | 3.06 | 100% | within budget |
| 1,500,000 | fanout | 501 | 2.22 | 4.91 | 100% | within budget |
| 1,500,000 | mesh | 251 | 3.10 | 4.94 | 100% | within budget |
| 2,000,000 | mesh | 201 | 1.84 | 4.09 | 100% | within budget |
| 2,000,000 | fanout | 1001 | 8.86 | 13.98 | 100% | within budget |
| 3,000,000 | explore | 301 | 2.38 | 4.18 | 100% | within budget |
| 3,588,000 | explore | 300 | 2.49 | 4.40 | 100% | within budget |
Every number is from a single local run on Apple M4 Pro, 14 cores, release build, gateway and load generator on the same machine — not a production deployment. Latency is service latency: message arrival minus actual send, the more conservative of the two figures the harness records. The highest load any run held inside the budget was 3,588,000 deliveries a second. Two topic-heavy runs, 5,000 topics and membership churn, also miss the budget, and the cause is not yet isolated: the load generator may be the bottleneck in the first case, and copying the subscriber list on every join and leave may be in the second. Every connection now owns an inbox, so memory per connection rose from about 150 KiB to about 200 KiB. Benchmark scripts and methodology are in the repository.
Protocol
Simple by design
No handshake. When authentication is on, the token rides the upgrade request, never an in-band frame. Connect, receive your ID, subscribe to a topic, start publishing. Four kinds of frame cover everything.
On connect, the server immediately sends a welcome frame with the client's assigned UUID. No handshake required — but nothing is delivered until you subscribe to a topic.
// Server → Client (on connect){ "type": "welcome", "id": "a3f1b2c4-5678-..."}Scope
Sharp boundaries, clear purpose
The scope test is simple: a feature belongs in the gateway only if it can be implemented without knowing what the payload means. Everything else belongs in your application.
In the gateway
- Connection management & lifecycle
- Message relay & envelope framing
- Backpressure & queue overflow handling
- Topic-based routing
- Per-connection rate limiting (after the MVP)
- Connect-time authentication & admission control
- Delivery semantics & acknowledgements
- Multi-instance backplane (not scheduled)
In your app
- Usernames, profiles, or identity
- Message content interpretation
- Chat history or persistence
- Business logic or domain rules
- Push notifications beyond WebSocket
- REST API endpoints for data
“If it needs to know what the message says, it doesn’t belong here.”
The Operatives
Four roles, four faces
Every connection in Akagitsune runs two concurrent tasks — reader and writer — around one shared topic registry, plus a lead that ties them together. Meet the cast.
Roadmap
What comes next
Akagitsune is under active development. Topic routing, connect-time authentication and publish pacing have landed. What is left is listed in the order it is planned, and every item still has to pass the scope test before it is implemented.
Done
Topic Routing
Achieved2026-10-06A publish reaches only the subscribers of its topic. The single broadcast bus is gone: clients subscribe to topics through a lock-free registry, with one bounded inbox per connection.
Connect-Time Authentication
Achieved2026-10-22A connection is admitted only with a configured token, sent as a bearer header or a subprotocol, never as an in-band frame. A missing or wrong token gets a 401 before any WebSocket exists.
Publish Pacing
Achieved2026-10-22A text publish may carry delay_ms, up to 120 s. The connection's own reader waits before fanning out, with no queue, so the connection's later frames wait behind it.
MVP
The first production use: one instance, behind a proxy that terminates TLS, reached only by the operator's own services, with an application that tolerates at-most-once delivery (no replay after a disconnect or a lag drop). These are what that use still needs, and each gets its own proposal.
Heartbeat and Idle Timeout
MVPThe gateway sends protocol pings and closes a connection that does not answer in time. Today a peer that vanishes without a FIN or RST, and only subscribes, is never dropped and keeps its memory.
Connection Cap
MVPA configured maximum of open connections; an upgrade above it is refused. Bounds memory against a client stuck in a reconnect loop.
Graceful Shutdown on SIGTERM
MVPToday only Ctrl+C (SIGINT) is handled, while docker stop and Kubernetes send SIGTERM. Open WebSockets must be closed within a bound, not waited on forever.
Health Check and Minimal Metrics
MVPA health route for the orchestrator or load balancer, plus at least the open-connection count and the dropped-message total.
TLS-Terminating Proxy
MVPThe gateway does not terminate TLS, so clients reach it through a proxy that serves wss://. Delivered as tested configuration and documentation, with no code change in the gateway.
Open-File Limit
MVPEach connection is a file descriptor, and many Linux defaults allow 1024. Document the required ulimit -n, check it at startup, or both.
Packaging
MVPA container image of the release binary, and an example deployment that runs it behind the TLS proxy.
After the MVP
Per-Connection Rate Limiting
After the MVPAdmission control, in scope, but not needed while only the operator's own services publish. It returns when publishers the operator does not control connect, measured in messages per second first.
Under consideration
Not planned yet. Each one needs a decision before it can be planned.
Subscription Authorization
Under considerationRemoved from the plan. The gateway is self-hosted and only the operator's own services reach it, so an admitted client is already trusted. It comes back only if an untrusted client, such as a browser, connects directly, which needs a credential issued per connection.
Delivery Acknowledgements
Under considerationOptional per-message ack/nack. Payload-agnostic, but it forces per-message state and a retry path where today there is none, so it needs a decision before any proposal.
Not scheduled: a backplane for running more than one instance. Listed so it is not forgotten, not because it is planned.