Skip to content

Introduction

Nexo is a high-performance, all-in-one message broker built in Rust. It unifies Caching, Pub/Sub, Queues, and Streams into a single binary with zero external dependencies.

The Problem

Modern event-driven backends suffer from Infrastructure Fatigue. A typical stack requires juggling multiple specialized services — one for caching, one for job queues, one for event streams, one for real-time messaging — each with its own container, protocol, configuration, and SDK.

On top of that, many teams rely on managed cloud services like AWS SQS/SNS, Azure Service Bus, or GCP Pub/Sub. This means your local development environment will never match production: you either run heavy emulators (LocalStack, Azure Service Bus Emulator) with partial fidelity, or mock everything and discover bugs only after deploy.

The operational overhead is disproportionate to the actual problems being solved. Keeping dev, staging and production in sync becomes a constant source of friction.

The Solution

Nexo is an all-in-one broker designed to make project setup, local development, and developer experience as smooth as possible. One binary, one TCP connection, one SDK — four communication models ready to use out of the box.

  • Unified: One TCP connection for Caching, Pub/Sub, Queues, and Streams.
  • Simple: Deploy a single binary. No clusters to manage. No JVMs to tune.
  • Fast: Built in Rust on top of Tokio for extreme throughput and incredibly low latency.
  • Consistent: Same setup locally and in production. One Docker container, one endpoint.
  • Zero Dependencies: No external databases, no JVM, no Erlang VM. Just one executable.

True Dev/Prod Parity

With Nexo, the binary you run on your laptop is the exact same binary you run in production. No emulators, no mocks, no "it works on my machine".

yaml
# docker-compose.yml (local dev)
services:
  nexo:
    image: emanuelepifani/nexo:latest
    ports: ["7654:7654"]

Same image, same protocol, same guarantees — from your laptop to your Kubernetes cluster. The dev loop stays fast, the surprises stay out of production.

Performance

Benchmarks run on MacBook Pro M4 (Single Node):

EngineThroughputLatency (p99)
Store4.5M ops/sec< 1 µs
PubSub3.8M msg/sec< 1 µs
Stream1.9M ops/sec< 1 µs
Queue400k ops/sec2 µs

Quick Example

typescript
import { NexoClient } from '@emanuelepifani/nexo-client';

const client = await NexoClient.connect({ host: 'localhost', port: 7654 });

// Store
await client.store.map.set("user:1", { name: "Max", role: "admin" });

// Pub/Sub
await client.pubsub('alerts').publish({ level: "high" });

// Queue
const q = await client.queue("emails").create();
await q.push({ to: "test@test.com" });

// Stream
const stream = await client.stream('events').create();
await stream.publish({ type: 'login', userId: 'u1' });
python
from nexo import NexoClient, NexoQueue, NexoStream, NexoTopic

client = await NexoClient.connect(host="localhost", port=7654)

# Store
await client.store.map.set("user:1", {"name": "Max", "role": "admin"})
user: User | None = await client.store.map.get("user:1")

# Pub/Sub
alerts: NexoTopic[Alert] = client.pubsub("alerts")
await alerts.publish({"level": "high"})

# Queue
q: NexoQueue[Email] = await client.queue("emails").create()
await q.push({"to": "test@test.com"})

# Stream
stream: NexoStream[Event] = await client.stream("events").create()
await stream.publish({"type": "login", "userId": "u1"})

When NOT to Use Nexo

Nexo is built for vertical deployments and developer experience, not for every scenario. It is NOT the right choice if:

  • You need multi-region replication — Nexo is a single-node broker. If you need geo-distributed replication, use Kafka or NATS with clustering.
  • You're at Kafka-scale throughput (>1M msg/sec sustained with multiple TB/day) — Nexo handles impressive throughput for a single node, but it won't replace a multi-broker Kafka cluster at petabyte scale.
  • You need exactly-once delivery semantics across distributed consumers — Nexo Queue provides at-least-once with acks and retries. If you need exactly-once across distributed systems, look elsewhere.

If none of the above applies to you, Nexo might be exactly what you're looking for.

Scaling

Nexo is deliberately designed not to scale horizontally — no distributed locks, no consensus protocols, no clustering layer. A single instance handles all four brokers on its own thread pools, scaling vertically.

This goes against the current trend of distributed-everything, but the reality is that most projects will never need horizontal scaling. Their backends bottleneck long before a single Rust-based broker does. For the vast majority of teams, this is more than enough.

If you do need to scale out, you can run multiple Nexo instances, each dedicated to a single broker (e.g. one for Store, one for Queue, one for Stream). This gives horizontal separation without the complexity of distributed coordination.

Delivery Model

Each broker has a fundamentally different way of delivering messages to the client:

BrokerDeliveryHow it works
StoreRequest/ResponseClient sends a command, server replies. Synchronous round-trip.
QueueLong-PollClient subscribes, server holds the request until a job is available, then responds. Client acks. Repeat.
StreamLong-PollSame as Queue — client subscribes, server holds until events are available at the consumer's offset.
Pub/SubServer-Side PushServer pushes messages to the client the instant they arrive. No polling, no waiting. Lowest latency.

This is why Pub/Sub is the lowest-latency primitive: there is no round-trip. The server pushes data the moment it arrives.

Persistence

BrokerStorageOn Restart
StoreRAMLost. Ephemeral cache / shared state.
Pub/SubRAM + SQLite (retained)Messages lost; retained topics restored.
QueueSQLiteSurvives. Jobs, state, and DLQ restored.
StreamBinary log filesSurvives. Full history with retention.

Broker Semantics

All brokers share a single TCP connection. One read loop demultiplexes incoming frames and routes them to the right broker. A slow consumer in one broker never blocks the others.

Store

Ideal use case: caching, session state, counters, feature flags.

Semantics: shared in-memory key/value store. Pure request/response — no callbacks, no consumer loop. Think Redis SET/GET/DEL.

  Client                         Server (RAM)
    │                                 │
    │── "SET user:1 Max" ───────────► │
    │◄────────── "OK" ────────────────│
    │                                 │
    │── "GET user:1" ───────────────► │
    │◄──────── "{name:Max}" ──────────│

Every operation is a request/response round-trip. No background tasks.

Queue

Ideal use case: email sending, PDF generation, background jobs — work that must not be lost.

Semantics: durable FIFO with acks. If No ack → N retry → after N retry -> dead-letter queue.

Producer publishes jobs one by one, in this order:

  push(j1)  push(j2)  push(j3)  push(j4)
     │         │         │         │
     ▼         ▼         ▼         ▼
  ┌───────────────────────────────────────┐
  │  Queue "emails" (FIFO)                │
  │  [j1] [j2] [j3] [j4]                  │
  └───────────────────┬───────────────────┘

                      │ long-poll: pull batch(3)

  ┌───────────────────────────────────────┐
  │  Consumer task (concurrency = 3)      │
  │  ┌─────┐  ┌─────┐  ┌─────┐            │
  │  │ j1  │  │ j2  │  │ j3  │            │
  │  └──┬──┘  └──┬──┘  └──┬──┘            │
  │     │        │        │               │
  │    ack      nack     ack              │
  │   (done)   (retry)  (done)            │
  └───────────────────────────────────────┘

  j4 stays in the queue, pulled in the next batch.
  Each job is independent: ack/nack decides its fate.

Stream

Ideal use case: event sourcing, audit trails, CDC — when you need to replay history.

Semantics: append-only log with offsets. Consumer groups track their own position. Ordered per key, parallel across different keys.

Producer publishes events, each with a key:

  publish("login",    key=A)
  publish("signup",   key=B)
  publish("logout",   key=A)
  publish("purchase", key=C)
         │         │         │         │
         ▼         ▼         ▼         ▼
  ┌─────────────────────────────────────────────┐
  │  Stream Log (append-only, immutable)        │
  │  offset 0: login     key=A                  │
  │  offset 1: signup    key=B                  │
  │  offset 2: logout    key=A                  │
  │  offset 3: purchase  key=C                  │
  └──────────────────────┬──────────────────────┘

                         │ consumer group "analytics"
                         │ reads from its offset

  ┌─────────────────────────────────────────────┐
  │  Same consumer group:                       │
  │  • key=A: [0:login] → [2:logout]  (ordered) │
  │  • key=B: [1:signup]              (ordered) │
  │  • key=C: [3:purchase]            (ordered) │
  │                                             │
  │  key=A and key=B run in parallel            │
  │  (different keys are independent)           │
  └─────────────────────────────────────────────┘

  Another consumer group "billing" can read the same log
  from a different offset, independently.

Same key = serial order. Different keys = parallel. Consumer groups read independently.

Pub/Sub

Ideal use case: real-time notifications, IoT heartbeats, config push — fire-and-forget events.

Semantics: transient topic broadcast. No ack, no retry, no persistence (except retained messages). Each subscription has its own queue + consumer task.

  Publisher

     │── publish "alerts" ────────────────────────►
     │                                              │
     ▼                                              ▼
  Server broadcasts to all matching subscribers:

  ┌─────────────────────┐      ┌─────────────────────┐
  │  subscription A     │      │  subscription B     │
  │  topic "alerts"     │      │  topic "metrics"    │
  │  queue: [m1, m2]    │      │  queue: [m1]        │
  │       │             │      │       │             │
  │       ▼             │      │       ▼             │
  │  cb(m1) → cb(m2)    │      │  cb(m1)             │
  │  (FIFO, serial)     │      │  (FIFO, serial)     │
  └─────────────────────┘      └─────────────────────┘

  subscription A and subscription B run in parallel
  but each one processes its own messages in order

Server pushes instantly. Each subscription has its own queue and consumer.