NATS : le messaging system qui va plus vite que ton réseau

Pourquoi jai remplacé Kafka par un binaire Go de 20 Mo

J’ai passé des années à faire tourner Kafka. C’est un outil génial, personne va dire le contraire. Mais putain, la complexité. ZooKeeper (ou KRaft), les brokers, les partitions, les consumer groups, les offsets — et tout ça pour quoi ? Pour envoyer des messages entre services.

Un jour j’ai découvert NATS. Et là, le choc. Mêmes fonctionnalités que Kafka pour l’event-driven, mais :

  • Un binaire Go unique de 20 Mo
  • 10 millions de messages par seconde sur un seul serveur
  • Un protocole texte avec 5 verbes
  • Pas de ZooKeeper, pas de KRaft, pas de partitions à configurer
  • Et JetStream, la couche persistence, intégrée dans le même binaire

J’ai déployé ça dans mon infra. Je vais pas revenir en arrière.

C’est là mes définitions et si elles vous déplaisent, cassez-vous je suis pas là pour discuter lexique.

  • NATS: Message broker écrit en Go. Rapide. Très rapide. Simple.
  • Core NATS: La couche de base. At-most-once. Pour le trafic qui peut perdre des messages.
  • JetStream: Persistence intégrée au serveur. Streams, consumers, exactly-once, KV store.
  • Stream: Un log ordonné de messages stockés sur disque.
  • Consumer: Un curseur dans un stream. Sait où t’en es. Scale horizontalement.
  • Subject: Le sujet NATS. Hiérarchique avec wildcards : orders.>.

Core NATS gère trois modèles de messaging. Et c’est tout ce dont t’as besoin.

Le plus simple des modèles. Tu publies sur un subject, tous les abonnés reçoivent.

// Publisher
nc.Publish("orders.created", data)

// Subscriber
nc.Subscribe("orders.created", func(msg *nats.Msg) {
    log.Printf("Reçu : %s", msg.Data)
})

Un client envoie une requête, NATS route vers un service qui répond. NATS gère le reply-to subject automatiquement.

// Requester
resp, err := nc.Request("service.calc", payload, 2*time.Second)

// Responder
nc.Subscribe("service.calc", func(msg *nats.Msg) {
    result := process(msg.Data)
    msg.Respond(result)
})

Plusieurs instances du même service partagent le même nom de queue. NATS distribue les messages automatiquement — load balancing sans config, sans partition à gérer.

// Trois instances : NATS répartit la charge
nc.QueueSubscribe("tasks", "workers", func(msg *nats.Msg) {
    process(msg.Data)
})

Le protocole NATS est tellement simple qu’on peut l’implémenter à la main :

PUB orders.created 22
{"order_id": "123"}
SUB orders.* 1
PING
PONG

Cinq verbes principaux : PUB, SUB, UNSUB, PING, PONG. Pas de handshake complexe, pas de frames binaires à décoder.

Les serveurs NATS se connectent en mesh complet. Chaque message fait au maximum un saut. Tu ajoutes ou retires un serveur sans downtime — le discovery protocol propage la topologie automatiquement.

[Server A]────[Server B]
     │            │
[Server C]────[Server D]

Le client se connecte à n’importe quel serveur. Si le serveur tombe, la librairie client reconnecte automatiquement vers un autre.


Le problème du Core NATS, c’est que c’est at-most-once. Si t’es pas abonné au moment de la publication, le message est perdu. Pour des métriques ou des notifications éphémères, c’est parfait. Mais pour des commandes, des paiements, des événements métiers, il faut de la persistence.

JetStream est intégré au même binaire NATS. Tu l’actives avec un flag --jetstream et t’as accès à des streams durables, du message replay, du exactly-once, et même un KV store et un object store.

Un stream capture les messages publiés sur un ou plusieurs sujets et les stocke dans un log ordonné.

Stream "orders" (sujets: orders.>)
├── orders.created.123  → seq 1
├── orders.shipped.123  → seq 2
├── orders.created.456  → seq 3
├── orders.delivered.123 → seq 4
└── orders.shipped.456  → seq 5
0$ nats stream add ORDERS \
1    --subjects "orders.>" \
2    --storage file \
3    --retention limits \
4    --max-msgs 1000000 \
5    --max-age 7d \
6    --replicas 3

Les politiques de rétention :

  • limits : les messages sont gardés selon les limites (max msgs, max bytes, max age)
  • interest : les messages sont gardés tant qu’il y a au moins un consommateur
  • workQueue : chaque message est consommé une seule fois (FIFO)

Un consumer est un curseur dans le stream. Il sait ce que t’as déjà lu, il gère les redélivrances.

Deux types :

  • Push : le serveur envoie les messages automatiquement. Pour la faible latence.
  • Pull : le client demande des batches. Pour le backpressure et le scaling horizontal.
0$ nats consumer add ORDERS processor \
1    --filter "orders.created.>" \
2    --pull \
3    --ack explicit \
4    --deliver all \
5    --max-deliver 3

Modes d’acknowledgement :

  • Ack : traité
  • Nak : pas traité, redélivré
  • InProgress : prolonge le délai (traitement long)
  • Term : arrête la redélivrance

JetStream propose trois niveaux de QoS :

Niveau Description Usage
At-most-once Core NATS, pas de persistence Métriques, dashboards
At-least-once JetStream + ack Files d’attente standard
Exactly-once Déduplication + double ack Paiements, commandes

Pour l’exactly-once, le publisher inclut un Nats-Msg-Id unique. Le serveur déduplique. Le consumer utilise un double ack. Si le publisher reçoit une erreur, il renvoie le même message avec le même ID — le serveur ignore le doublon.

JetStream inclut un KV Store intégré :

kv, _ := js.CreateKeyValue(&nats.KeyValueConfig{
    Bucket: "sessions",
})
kv.Update("user:123", "session_data", 42)  // CAS

Et un Object Store pour stocker des fichiers volumineux (chunking automatique). Idéal pour distribuer des artefacts ou des configurations sans monter un S3.


Critère NATS JetStream Kafka
Architecture Binaire unique ZooKeeper/KRaft + Brokers
Protocole Texte simple (5 verbes) Binaire complexe
Performance 10M+ msg/s par serveur 1M+ msg/s par broker
Latence Microsecondes (Core) / ms (JetStream) 5-10ms
Partitions Sujets hiérarchiques, pas de partitions Partitions à configurer
Complexité Faible. Tu lances et ça marche. Élevée. Cluster à maintenir.
KV Store Intégré (JetStream KV) Aucun
Object Store Intégré (JetStream Object) Aucun

J’avais un cluster Kafka pour gérer les événements de mon infra. Trois brokers, un ZooKeeper, des partitions à configurer, des offsets à monitorer. Ça marchait, mais ça pesait.

J’ai tout migré sur NATS. Un binaire. Un flag --jetstream. Les mêmes streams. Le même exactly-once. En mieux : le KV store remplace Redis pour mes sessions, l’object store remplace un stockage S3 pour mes artefacts.

Et les performances ? Core NATS c’est des microsecondes de latence. JetStream c’est des millisecondes. Kafka c’est 5 à 10ms dans le meilleur des cas.

La vérité, c’est que NATS est sous-côté parce que les gens pensent que pour du messaging fiable, faut du Kafka. Mais JetStream a tout ce qu’il faut : persistence, replay, exactly-once, clustering. Sans la complexité.

Si tu montes une architecture event-driven, commence par NATS. Si un jour t’as besoin de persistence, t’actives JetStream. Pas de migration. Pas de nouveau déploiement. Le même binaire.

See ya space-cowboy!