Akagitsune banner — cyberpunk cityscape with crimson glow
Akagitsune logo

Akagitsune

赤狐

A generic realtime WebSocket gateway built in Rust. Pure transport — it relays, it does not interpret.

1M/s
Deliveries sustained
6ms
Worst p99 at 1M/s, one topic
100%
Delivered at 1M/s
View on GitHub

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

$ cargo run
# Connect at ws://127.0.0.1:3000/ws

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.

Benchmark runs by offered load and traffic shape, with p50 and p99 service latency, delivery rate, and whether each run stayed inside the 20 ms budget.
Offered loadShapeConnsp50p99DeliveredAgainst budget
90,000topics10002.745.10100%within budget
124,750churn5005.3050.30100%over budget
450,000topics500046.2171.42100%over budget
1,000,000ingest1011.994.53100%within budget
1,000,000fanout5011.844.72100%within budget
1,000,000mesh2013.105.97100%within budget
1,000,000topics210018.6227.84100%over budget
1,500,000ingest1011.583.06100%within budget
1,500,000fanout5012.224.91100%within budget
1,500,000mesh2513.104.94100%within budget
2,000,000mesh2011.844.09100%within budget
2,000,000fanout10018.8613.98100%within budget
3,000,000explore3012.384.18100%within budget
3,588,000explore3002.494.40100%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-06

A 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-22

A 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-22

A 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

MVP

The 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

MVP

A 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

MVP

Today 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

MVP

A health route for the orchestrator or load balancer, plus at least the open-connection count and the dropped-message total.

TLS-Terminating Proxy

MVP

The 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

MVP

Each connection is a file descriptor, and many Linux defaults allow 1024. Document the required ulimit -n, check it at startup, or both.

Packaging

MVP

A 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 MVP

Admission 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 consideration

Removed 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 consideration

Optional 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.