← Back to the blog

NATS vs Kafka for Go: an honest comparison

Kafka is the default choice for event streaming. NATS is the thing you pick when you've run Kafka once. Here's when each is actually right.

Kafka is what teams reach for when they hear "event streaming." It's the default, the safe choice, the one your last job used. NATS is what teams reach for after they've run Kafka once and learned that most of their use case didn't need it. This is a comparison from building event-driven systems in Go with both.

Quick answerUse NATS JetStream for service-to-service messaging, task queues, and event buses inside a single system or cluster. Use Kafka when you need long-term retention, large-scale replay, or are building a data pipeline consumed by multiple downstream systems over months.

The actual choice

The question isn't Kafka vs NATS in the abstract. It's: what does this system actually need?Most microservice communication patterns — service events, task queues, command dispatch, notification flows — don't need Kafka's retention guarantees or its partition model. They need reliable delivery, at-least-once or exactly-once semantics, and operational simplicity. NATS JetStream delivers all three at a fraction of the infrastructure cost.

NATS and JetStream

Core NATS is a publish-subscribe message bus: fast, ephemeral, no persistence. JetStream adds persistence — messages are stored on disk and can be replayed by consumers that join late or re-read from an offset.

What makes NATS compelling for Go services:

  • Written in Go, fits naturally in a Go ecosystem.
  • Single binary, single config file. No Zookeeper, no broker cluster management at small scale.
  • Sub-millisecond latency for service-to-service messaging.
  • JetStream consumers support push and pull models, durable subscriptions, and exactly-once delivery with deduplication.
  • Built-in key-value store and object store for free.

Kafka

Kafka's model is fundamentally different. Topics are partitioned, ordered, durable logs. Consumers read from an offset and can replay arbitrarily. Kafka keeps messages for days or weeks; consumers can catch up from the beginning of the log at any time.

Kafka 4.0 dropped ZooKeeper (KRaft mode is now the default), which removed its largest operational complaint. But it's still a JVM process with real memory requirements, a complex partition-rebalancing story, and tuning knobs that take time to understand.

What Kafka is genuinely best at:

  • High-throughput data pipelines: millions of events per second.
  • Long-term event retention for audit, compliance, or analytics.
  • Multiple independent consumers reading the same stream at different offsets.
  • Fan-out to Flink, Spark, or a data warehouse.

The comparison

// NATS JetStream
Latency:      sub-millisecond
Throughput:   high (millions/sec per node)
Persistence:  JetStream (configurable retention)
Operational:  single binary, minimal config
Best for:     service messaging, task queues, command bus

// Kafka
Latency:      low ms (higher than NATS)
Throughput:   very high (millions/sec, horizontally scalable)
Persistence:  log-based, long retention (days to forever)
Operational:  JVM, cluster config, partition tuning
Best for:     data pipelines, audit logs, multi-consumer streams

NATS in Go

The official NATS Go client is excellent. Publishing and consuming a JetStream subject:

nc, _ := nats.Connect(nats.DefaultURL)
js, _ := jetstream.New(nc)

// Create a stream (idempotent).
stream, _ := js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
  Name:     "INVOICES",
  Subjects: []string{"invoices.>"},
  Retention: jetstream.WorkQueuePolicy,
})

// Publish.
js.Publish(ctx, "invoices.created", payload)

// Consume with at-least-once delivery.
consumer, _ := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{
  Durable:   "invoice-processor",
  AckPolicy: jetstream.AckExplicitPolicy,
})
consumer.Consume(func(msg jetstream.Msg) {
  // process
  msg.Ack()
})

Our recommendation

For the vast majority of Go backend architectures: start with NATS JetStream. The operational story is dramatically simpler, the Go client is first-class, and it handles almost every service messaging pattern. You can run NATS in a single container alongside your services.

Reach for Kafka when you have a genuine data pipeline problem — multiple consumers across multiple teams reading the same stream, long retention requirements, or downstream analytics. Don't start there.

If you're designing the event architecture for a new system, we do architecture reviewsbefore teams commit to infrastructure they'll carry for years.

★ ★ ★

End of article · Thanks for reading

Subscribe

More of this, once a month.