All articles
Infrastructure·April 26, 2026·7 min read

NATS vs Redis Streams: Choosing a Trading-Bot Event Bus

NATS vs Redis Streams for a trading bot: a latency and durability shootout for the internal event bus fanning market data to strategy, execution and risk.

The moment your bot outgrows a single process, you need a bus. One strategy loop reading a websocket and firing orders is fine as a monolith. The day you split market data, three strategies, an execution gateway and a risk kill-switch into separate services, something has to carry events between them without dropping ticks or adding 5ms of jitter to every fill decision. NATS and Redis Streams are the two tools most teams reach for, and they behave nothing alike under load.

I've run both in production for market-making and arbitrage systems. Here's how they actually compare when the payload is order-book deltas and the consumer is a strategy that will misquote if the bus stalls.

The two models, honestly

Core NATS is a fire-and-forget pub/sub broker written in Go. A publisher sends to a subject like md.BTCUSDT.book, every subscriber to that subject gets a copy, and the server keeps nothing afterward. If a subscriber falls behind, NATS disconnects it (slow-consumer detection) rather than buffering forever. There is no replay. That's the mode you want for hot market data, where a 40ms-old book update is already garbage.

JetStream is NATS's persistence layer on top of that core. It adds streams, at-least-once delivery (exactly-once with a dedup window), replay by sequence or timestamp, and consumer acks. You opt into it per subject. So "NATS" is really two products: a sub-microsecond ephemeral bus, and a durable log that competes head-to-head with the next contender.

Redis Streams is an append-only log data structure living inside Redis. XADD appends an entry with an auto ID (1719840000000-0 — a millisecond timestamp plus a sequence counter). Consumers either XREAD from an offset or join a consumer group with XREADGROUP, which gives you per-consumer cursors, pending-entry tracking and XACK. It's durable to the same degree Redis is: RDB snapshots and the AOF, which is a very different durability story from a purpose-built log.

Latency: where they actually differ

For fan-out of ephemeral ticks, core NATS wins and it isn't close. Publish-to-deliver on a loopback or same-rack setup sits in the 30–80µs range, and it stays flat as you add subscribers because the server just copies buffers. Redis Streams routes everything through a single-threaded event loop. Under a normal load an XADD plus a blocking XREADGROUP round-trips in a few hundred microseconds, but that number is a function of how busy the Redis instance is with everything else you've crammed into it — and teams cram a lot into Redis.

The gotcha that bites people: if you're already using that Redis box for your rate-limit counters, a hot KEYS scan from some admin script, or a fat HGETALL, your market-data stream inherits that tail latency because it's the same thread. NATS doesn't have a shared-blocking-thread problem. This is the single biggest reason I keep hot-path market data off Redis on busy systems. Getting the wiring right here is most of what a clean market-data and infrastructure build is about.

Durability: where Redis Streams earns its keep

The tradeoff flips for events you cannot lose. Fills, order acks, position deltas, risk-limit breaches — replaying those after a crash is the difference between a reconciled book and a manual mess at 3am. Both JetStream and Redis Streams give you a replayable log with acks. The practical questions are backpressure and trim policy.

Redis Streams needs a capped length or it grows until it eats your RAM, because Redis is in-memory first:

XADD fills MAXLEN ~ 1000000 * side buy px 64150.5 qty 0.25

That ~ matters. It tells Redis to trim approximately, in whole radix-tree nodes, which is O(1)-ish. Drop the ~ and you get exact trimming that walks entries and adds latency on the hot path. I've seen a p99 spike traced back to someone using exact MAXLEN on a high-rate stream.

JetStream trims by policy on the server: MaxAge, MaxBytes, MaxMsgs, with Limits, WorkQueue or Interest retention. A work-queue stream deletes a message once it's acked, which is exactly what you want for an execution job queue where each fill event must be processed by precisely one execution worker.

A concrete fan-out topology

Here's the split I keep landing on for a mid-frequency system:

  • Hot market data → core NATS pub/sub. Subjects like md.{venue}.{symbol}.book and md.{venue}.{symbol}.trades. Wildcard subscriptions (md.binance.*.book) let a strategy grab a whole venue with one subscription. No persistence, no acks, no replay — if a strategy misses a tick it resnapshots from the REST book. Losing an update is cheaper than the latency of guaranteeing delivery.
  • Execution and risk events → JetStream (or Redis Streams if Redis is already your source of truth). orders.new, orders.ack, fills, risk.breach. Durable, acked, replayable. The risk service replays from the last acked sequence on restart and rebuilds live exposure before it re-arms the kill-switch.

A minimal NATS publisher for the hot path in Python:

import asyncio, orjson, nats

async def main():
    nc = await nats.connect("nats://10.0.0.5:4222")
    async def on_book(update):
        await nc.publish(
            f"md.binance.{update['s']}.book",
            orjson.dumps(update),
        )
    # feed on_book from your websocket handler
    await run_ws(on_book)

asyncio.run(main())

Note orjson over stdlib json — at a few thousand book updates a second, serialization is not free, and swapping the encoder shaved real microseconds off my publish path. Same idea whether the bus underneath is NATS or Redis.

Ordering and the redelivery trap

Both systems give you per-key ordering only if you're careful. In Redis Streams, a single stream is totally ordered, but if you shard fills across fills.{symbol} for throughput, cross-symbol ordering is gone. Fine for isolated books, wrong if your risk engine nets across correlated legs.

JetStream redelivers unacked messages after AckWait. Set that too low and a strategy that's mid-GC pause gets a duplicate fill event, double-counts a position, and quotes off a phantom inventory. Make your consumers idempotent — key on the venue order ID, not on arrival — and treat every "exactly once" claim as "at least once, plus your dedup." This is the class of bug that looks like a strategy problem and is actually a bus config problem. If your fills are landing on a live trading dashboard, a duplicated event there is the first place you'll notice the drift.

What I'd pick

  • Pure speed, ephemeral fan-out, willing to resnapshot on gaps → core NATS. Nothing beats it for spraying order books at a fleet of strategies.
  • Already all-in on Redis, moderate rates, want one box to reason about → Redis Streams. Consumer groups and XPENDING are genuinely nice, and one fewer system to run has real value on a small team.
  • You need both hot fan-out and a durable event log from one system → NATS with JetStream. Run core subjects for market data and JetStream streams for execution, on the same cluster.

On a Solana venue the calculus shifts again, because your bus latency competes with landing time on-chain. It's not much use shaving 200µs off internal fan-out if your transactions sit in the mempool — that's why staked routing like SWQoS staked RPC connections and dual submission across sendTransaction and Jito bundles matter more than the broker choice, and why sending straight to the leader via a QUIC TPU client can dominate the whole latency budget. Get the on-chain path handled and the internal bus becomes the easy part of a Solana market-making stack.

Whichever you pick, instrument the bus itself — publish timestamp in the payload, measure delivery lag at every consumer, alert when p99 drifts. A silent slow consumer on NATS or a growing XPENDING on Redis is the kind of thing that stays invisible until it costs you a bad fill.

If you want a second set of eyes on an event bus that's already dropping ticks or adding jitter under load, that's exactly the sort of thing our ongoing support and maintenance work is built to catch.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#NATS#Redis Streams#event bus#low latency#trading infrastructure