Event-Driven Architecture with NATS · Part 6

NATS vs Kafka vs AWS SQS: A Decision Framework for Engineers

An honest comparison of delivery semantics, persistence, ops burden, fan-out, replay, portability, and cost — with symptom-based selection tables instead of benchmark wars.

16 min read

You have read how NATS subjects, JetStream streams, KV mirrors, CQRS projections, and observability fit together in Parts 1–5 of this series. You have seen where a message bus sits in a growing architecture in The Pieces of Modern, Effective Software Design. The remaining question is blunt: when is NATS the right bus, and when should you reach for Kafka or AWS SQS instead?

This article is a decision framework, not a winner-takes-all shootout. Claims below are tied to official documentation. Where a row reflects engineering judgment — for example, typical ops burden in a small team — that is labeled as inference.

Researched against docs.nats.io, Apache Kafka documentation, and AWS SQS documentation on July 6, 2026.

TL;DR

  • Core NATS is at-most-once, in-memory pub/sub with request/reply and queue groups — excellent for live internal messaging when subscribers are online.
  • JetStream (built into nats-server) adds durable streams, replay, KV, and at-least-once / opt-in exactly-once semantics — the NATS answer to temporal decoupling without adopting a separate platform.
  • Kafka is a distributed event log: topics are partitioned, retained, and replayable by design; stream processing (Kafka Streams, Connect, ecosystem) is the main reason teams adopt it.
  • SQS is a managed point-to-point queue on AWS: no cluster to run, at-least-once standard queues, FIFO with exactly-once processing when you need ordering — not a log and not native pub/sub.
  • Pick on symptoms (offline consumers, replay window, fan-out shape, cloud lock-in tolerance, who runs the cluster), not on unsourced latency leaderboards.

What You Will Learn Here

  • How NATS (Core + JetStream), Kafka, and SQS differ on eight engineering dimensions
  • When NATS wins — including patterns from Part 1 (request/reply, queue groups), Part 2 (streams/consumers), Part 3 (KV), and Part 4 (internal plumbing)
  • When Kafka wins — log retention, partitions, stream processing
  • When SQS wins — AWS-native decoupling without broker operations
  • An ASCII decision flow and symptom → fit tables you can paste into an ADR

Series context: Part 1 · Part 2 · Part 3 · Part 4 · Part 5

Three Different Primitives

Before the comparison table, align on what each system is:

SystemPrimary abstractionMental model
Core NATSSubject-based pub/subFire-and-forget routing; subscribers must be online
JetStreamDurable stream + consumerCapture, replay, ack-based delivery on top of NATS transport
KafkaPartitioned commit logAppend-only topics; consumers track offsets; retention is a first-class knob
SQSHosted queueProducer enqueues; one consumer group processes; message deleted after ack
  Core NATS          JetStream                 Kafka                    SQS
  ─────────          ─────────                 ─────                    ───
  Pub ──► Sub        Pub ──► Stream ──► Cons  Pub ──► Topic (log)      Pub ──► Queue ──► Worker
  (live only)        (persist + replay)        ──► many consumers       (point-to-point)
                                               (offsets, retention)

SQS is not a drop-in substitute for Kafka or NATS pub/sub. AWS documents SQS as decoupling via queues; fan-out to many subscribers typically pairs SNS → SQS (SQS overview).

Comparison Dimensions

Delivery semantics

DimensionCore NATSJetStreamKafkaSQS (standard)SQS (FIFO)
Default guaranteeAt-most-once, best-effort (What is NATS?)At-least-once; work-queue retention can target exactly-once consumption per message (JetStream)At-least-once by default; idempotent producer + transactions enable exactly-once within Kafka when configured (Kafka design — semantics)At-least-once (SQS queue types)Exactly-once processing with deduplication (SQS queue types)
Offline subscriberMessage lostReplay from streamRead from retained logMessage waits in queue (until retention)Same
Idempotency expectationAlways design for itAlways design for itRequired for at-least-once pathsRequired (duplicates possible on standard)Less duplicate risk; still validate

Engineering note: “Exactly-once” anywhere usually means “exactly-once under documented failure models.” Kafka’s docs are explicit that end-to-end exactly-once to external systems requires cooperation with those systems.

Persistence model

DimensionCore NATSJetStreamKafkaSQS
StorageIn-memory only; never written to disk (What is NATS?)File or memory streams; retention limits (max_age, max_bytes, discard policy)Log segments on broker disks; retention per topicRedundant storage across AZs; default retention 4 days, max 14 days (SQS overview)
Long-term archiveNoBounded by stream limitsYes — weeks/months common; “store data for a long time is perfectly fine” per Kafka design docsNo — consumption deletes; not an analytics log
OrderingNo global orderPer-subject order within a streamPer-partition total orderFIFO queues: order within message group

Operations complexity

DimensionNATS (self-hosted)Kafka (self-hosted)SQS
Process to runSingle nats-server binary; JetStream enabled in configMulti-broker cluster; ZooKeeper deprecated — KRaft mode in modern versionsNone — fully managed API
Scaling modelCluster + JetStream RAFT replication (1–5 replicas per stream)Partitions, brokers, ISR, rebalancingAutomatic; API rate limits per queue type
Day-2 concernsTLS, accounts/JWT, stream/consumer tuning, backup of JetStream storePartition planning, consumer lag, broker disk, connector ops, upgradesIAM, DLQ redrive, visibility timeout tuning, quota increases
Inference: smallest team burdenLower for Core NATS; moderate with JetStream HAHigher — plan for a platform-minded ownerLowest ops surface if already on AWS

NATS documents nats-server as “less than 20 MB” (What is NATS?). That speaks to deployment footprint, not a performance claim.

Fan-out vs queue

PatternCore NATSJetStreamKafkaSQS
Pub/sub fan-outNative — every subscriber gets a copyNative — multiple consumers on a streamNative — multiple consumer groups per topicNot native — use SNS fan-out to multiple queues
Work queue / competing consumersQueue groupsWork-queue retention + queue consumersConsumer group per partition assignmentOne consumer receives per in-flight message (visibility timeout)
Request/reply RPCBuilt-in inbox pattern (Part 1)Possible but not the sweet spotUnusual — not the primary APINot supported

Replay

CapabilityCore NATSJetStreamKafkaSQS
Replay after the factNoYes — by sequence, time, last message, replay speed (JetStream)Yes — consumer resets offset; events not deleted on read (Kafka intro)No meaningful replay — messages are consumed and deleted
Multiple independent read positionsN/AMultiple consumers per streamMultiple consumer groupsN/A (queue semantics)
Reprocess production traffic slowlyNooriginal replay policyReset offsets / new consumer groupNot applicable

Latency profile (qualitative only)

Official docs describe mechanisms, not cross-vendor latency rankings. Reason qualitatively:

FactorNATS CoreJetStreamKafkaSQS
Path lengthSubject route, in-memoryPersist → replicate → deliverBatch-oriented log append/fetchHTTPS API + managed storage
Typical sweet spot (inference)Live control-plane traffic, RPCDurable internal events with modest retentionHigh-volume pipelines with batchingAsync job handoff across AWS services
What not to doClaim “X ms faster than Y” without your own benchmarks in your networkSameSameSame

For SQS, polling vs long polling and visibility timeout directly affect observed end-to-end delay (message lifecycle) — tune these before swapping brokers.

Multi-cloud / portability

DimensionNATSKafkaSQS
Run anywhereYes — binary on bare metal, VMs, K8s, edge (What is NATS?)Yes — self-managed or vendor-managed (MSK, Confluent, Aiven, etc.)AWS only
Managed offeringsSynadia NGS, various cloudsManyNative AWS
Protocol couplingNATS wire protocolKafka protocol + rich ecosystemAWS SDK / HTTP API
Inference: exit costLow if self-hosted; higher if tied to one managed NATS vendorModerate — ecosystem creates gravityHigh — deepest integration, hardest to lift-and-shift

Cost model (qualify every claim)

Cost driverNATS (self-hosted)Kafka (self-hosted)SQS
MeteringInfra you pay for (CPU, disk, network) — no per-message license from the OSS serverSame + often larger disk footprint for retentionPer-request pricing; 1 MiB payload = up to 16 request units (SQS pricing)
Free tierN/A (your cloud bill)N/A1 million requests/month (SQS pricing)
Hidden costsHA replicas, monitoring, on-callConnector VMs, schema registry, larger clustersKMS, cross-region transfer, S3 extended client for large payloads
InferenceCost-effective at steady internal volume on existing K8sCost-effective when log retention replaces other storage pipelinesCost-effective for spiky AWS glue code if request count stays bounded

Do not extrapolate “cheapest” from list prices alone — model requests, payload size, retention GB-months, and engineer time.

When NATS Wins

NATS fits when messaging is internal infrastructure — service-to-service, edge-to-cloud, control plane — and you want one lightweight runtime that spans Core pub/sub and JetStream durability.

SymptomNATS fitSeries / doc anchor
Services need RPC without HTTP everywhereCore request/replyPart 1
Workers must share load on one subjectQueue groups or JetStream work-queue streamsPart 1, Part 2
Subscribers go offline briefly; events must surviveJetStream streams + durable consumersPart 2
Latest entity state + change notificationsJetStream KV watchPart 3
Postgres is truth; bus is plumbing to search/warehouseCDC → KV → index subjectsPart 4
Same security context from edge to cloudNATS 2.0 accounts/JWT; single binaryWhat is NATS?
You want persistence without a separate “streaming platform” teamJetStream built into nats-serverJetStream
  Internal mesh style (NATS sweet spot)

  API ──request/reply──► Authz svc

    └──publish──► orders.created ──► [billing worker]
                              └──► [analytics projector]
         JetStream stream (optional durability)
              └──► KV mirror ──watch──► indexer

When NATS is a weaker default (inference):

  • Multi-team analytics on months of immutable history with SQL-over-streams tooling
  • Organization standard is already Kafka Connect + Flink + schema registry
  • Team has zero appetite to operate any message broker — SQS/Lambda may win on ops alone

When Kafka Wins

Kafka fits when the log itself is the product — high-volume ingestion, many downstream consumers at different speeds, long retention, stream processing.

SymptomKafka fitDoc anchor
Many teams read the same topic at different offsetsConsumer groups + retained logKafka intro — topics
Event sourcing / audit with replay months laterConfigurable retention; disk-oriented designKafka design — persistence
Stream joins, windows, aggregations in the bus layerKafka Streams APIKafka APIs
Ingest from Postgres, S3, Elasticsearch via managed connectorsKafka ConnectKafka APIs — Connect
Partition-key locality (per-user ordering)Key → partition mappingKafka design — producer
Exactly-once between Kafka topics in a pipelineTransactional producer + read_committedKafka semantics
  Log-centric analytics (Kafka sweet spot)

  Producers ──► topic (partitions P0..Pn, replicated)

        ┌───────────┼───────────┬──────────────┐
        ▼           ▼           ▼              ▼
   Warehouse    Flink job   Search sink    Audit archive
   (Connect)    (Streams)   (Connect)      (long retention)

Tradeoffs to accept: broker count, partition planning, consumer lag dashboards, and upgrade discipline. Kafka’s own design docs target “high volume event streams” and “periodic data loads from offline systems” — not minimal internal RPC.

When SQS Wins

SQS fits when workloads are AWS-native, queue-shaped, and you want AWS to own availability.

SymptomSQS fitDoc anchor
Decouple Lambda, ECS, EC2 workersStandard queue + event source mappingSQS overview
No team to run Kafka/NATSFully managedSQS overview
Failed messages need inspectionDead-letter queues + redriveSQS DLQ
Strict per-entity orderingFIFO + message group IDSQS queue types
Fan-out to many workers on AWSSNS → multiple SQS queuesSQS vs SNS
Retention measured in days, not monthsDefault 4d, max 14d retentionSQS overview
  AWS glue (SQS sweet spot)

  API Gateway / ALB ──► SQS ──► Lambda / ECS worker

                      └── on failure ──► DLQ ──► redrive / alarm

When SQS is a weaker default (inference):

  • Native pub/sub with dozens of live subscribers on the same subject
  • Replay of arbitrary historical offsets for new consumers
  • Multi-cloud or on-prem primary deployment
  • Request/reply between internal microservices (use HTTP/gRPC or NATS Core instead)

Decision Flow

                        Start: async communication needed


                    ┌─────────────────────────────────────┐
                    │ Must run outside AWS or multi-cloud? │
                    └─────────────────────────────────────┘
                          │ yes                    │ no
                          ▼                        ▼
              ┌───────────────────┐      ┌──────────────────────┐
              │ Need log retention │      │ Queue + AWS services  │
              │ + stream ecosystem│      │ only?                 │
              └───────────────────┘      └──────────────────────┘
                 │ yes      │ no              │ yes      │ no
                 ▼          ▼                 ▼          ▼
            ┌────────┐  ┌─────────────────────────┐   ┌────────┐
            │ Kafka  │  │ Subscribers often offline │   │ SQS    │
            │        │  │ or need replay/KV/RPC?    │   │ (+SNS  │
            └────────┘  └─────────────────────────┘   │ if fan-│
                              │ yes      │ no        │ out)   │
                              ▼          ▼           └────────┘
                         ┌─────────┐  ┌──────────┐
                         │JetStream│  │Core NATS │
                         │(+ Core  │  │(live fan-│
                         │ as RPC) │  │out, RPC) │
                         └─────────┘  └──────────┘

Concrete Implementation: Symptom-Based Selection Guide

Use this section as an ADR appendix. Each row maps a production symptom to a reasonable default and the first config knob to verify.

Symptom → fit (messaging pattern)

SymptomReasonable defaultFirst knob to check
”Billing must charge exactly once per order ID”JetStream work-queue stream or SQS FIFO with dedup or Kafka idempotent producer + idempotent consumerIdempotency key in your handler either way
”Search indexer can lag 30s; API reads Postgres”JetStream or Kafka for change feed; not Core NATS aloneConsumer ack wait / max deliver (Part 2)
“10 microservices need the same fire-and-forget event live”Core NATS pub/subPlain vs queue subscribers (Part 1)
“New team wants SQL over 90 days of clicks”Kafka (+ warehouse sink)Topic retention bytes / time
”Lambda processes uploads overnight”SQS standardVisibility timeout ≥ p99 handler duration
”Service A calls Service B synchronously internal”Core NATS request/replyTimeout + ErrNoResponders handling
”Share latest config per region”JetStream KV + watchBucket replica count (Part 3)
“Exactly-once from topic A to topic B”Kafka transactionsisolation.level=read_committed
”Fan-out to 50 SQS queues on AWS”SNS → SQSQueue policy per subscriber

Symptom → fit (operational constraints)

SymptomLean towardWhy (source-backed or inference)
“We have no SRE for messaging”SQS on AWSManaged service (SQS overview) — inference for team size
”We already run K8s and want one small binary”NATSSingle server process (What is NATS?)
”Compliance wants immutable audit log 1 year”KafkaRetention model (Kafka intro)
“Burst to millions/sec with AWS billing OK”SQS standardScales without provisioning (SQS overview)
“Edge devices + cloud same bus”NATSDeployment model (What is NATS?) — inference for edge

Minimal wiring examples (selection smoke test)

NATS JetStream — durable order event (from series context):

nats stream add ORDERS \
  --subjects "orders.>" \
  --storage file \
  --retention limits \
  --max-age 7d \
  --replicas 3

Kafka — topic with explicit retention (illustrative):

# server.properties / topic config
log.retention.hours=168
min.insync.replicas=2

SQS — visibility timeout aligned to handler (AWS CLI):

aws sqs set-queue-attributes \
  --queue-url "$QUEUE_URL" \
  --attributes VisibilityTimeout=300

If you cannot articulate which row in the tables above matches your symptom, you are not ready to pick a broker — define the delivery guarantee and retention window first.

Hybrid Architectures (Common in Practice)

Real systems mix tools:

PatternComposition
AWS ingress, internal busALB → SQS → bridge service → NATS JetStream
Analytics off internal eventsNATS JetStream → (connector) → Kafka → warehouse
Legacy lift to cloudOn-prem NATS ↔ supercluster ↔ cloud NATS; SQS only at AWS boundary

Hybrids add operational seams — document ownership of retry, DLQ, and schema at each boundary.

Common Mistakes

  1. Choosing Kafka for internal RPC. Request/reply is not the Kafka sweet spot; use HTTP/gRPC or NATS Core.
  2. Choosing SQS for pub/sub. Use SNS fan-out or a bus with native subjects/topics.
  3. Using Core NATS for billing events. At-most-once + offline subscribers = silent loss (What is NATS?).
  4. Expecting SQS replay. Once deleted, it is gone — not a log.
  5. Ignoring idempotency. At-least-once is the default almost everywhere.
  6. Benchmark shopping. Vendor marketing and stale blog posts are not your network topology.

How This Fits the Series

PartFocus
1Core NATS primitives
2JetStream durability
3KV as change stream
4CQRS projections with NATS plumbing
5Observability and SLOs
6 (this article)When NATS vs Kafka vs SQS

For the architectural ladder (outbox, projections, search), see The Pieces of Modern, Effective Software Design.

Sources