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.
Lexique
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 : la base ultra-rapide
Core NATS gère trois modèles de messaging. Et c’est tout ce dont t’as besoin.
Publish/Subscribe
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)
})
Request/Reply
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)
})
Queue Groups
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
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.
Le clustering
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.
JetStream : la persistence sans la douleur
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.
Les streams
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 3Les 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)
Les consumers
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 3Modes d’acknowledgement :
- Ack : traité
- Nak : pas traité, redélivré
- InProgress : prolonge le délai (traitement long)
- Term : arrête la redélivrance
Exactly-once delivery
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.
KV Store et Object Store
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.
NATS vs Kafka : le match
| 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 |
Pourquoi j’ai migré
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!