NATS: A Lifesaver for Microservices When Kafka and RabbitMQ Are Too Heavy

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Why is NATS so ‘hot’ among Message Brokers?

If you are struggling with a Kafka cluster consuming dozens of GBs of RAM or RabbitMQ’s messy plugin configurations, NATS will be a completely different experience. I first encountered NATS while refactoring an IoT system. At that time, the server only had 2GB of RAM but had to handle thousands of devices sending messages continuously with minimal latency.

NATS is written in Go and packaged into a single binary file. Its philosophy is clear: “Simplify instead of complicate”. Instead of trying to do everything, NATS focuses on pushing messages as fast as possible.

Real-world Comparison: NATS vs. RabbitMQ vs. Kafka

Here is a summary based on my practical experience deploying all three systems:

  • RabbitMQ (Versatile but bulky): Very strong in complex routing. However, when traffic spikes, the Erlang VM consumes a massive amount of CPU. Managing a RabbitMQ Cluster is also a significant challenge for DevOps engineers.
  • Kafka (The Heavy-duty Truck): Unbeatable for storing terabytes of data. However, installing Kafka along with Zookeeper (or KRaft) is a “nightmare” for small projects. You need at least 1-2GB of RAM just for the Broker to… breathe.
  • NATS (The Racing Motorcycle): The binary file is only about 15MB. It starts up in milliseconds. RAM consumption is around 20MB at idle. This is the number one choice for Cloud Native and Edge Computing.

Real numbers: I once replaced a 3-node Kafka cluster (4GB RAM per node) with NATS JetStream for a logging system. The result: equivalent throughput but resource consumption dropped by 90%. Sometimes, compact is truly better.

Deploying NATS in a Snap

Installing NATS is so easy it will surprise you. You can choose to run it via Docker or directly as a binary.

1. Quick Start with Docker

docker run -d --name nats-server -p 4222:4222 -p 8222:8222 nats:latest

Quick Port Explanation:

  • Port 4222: The main communication port for Clients.
  • Port 8222: The HTTP port for checking system health via a browser.

2. Using NATS CLI for Quick Testing

To fire off a test message without writing code, install the nats tool. On Mac, you can simply use Brew:

# Quick installation
brew install nats-io/nats-tools/nats

# Test a message
nats pub my.subject "Hello NATS from ITfromZero"

Configuring JetStream: When You Need Data Persistence

NATS has two modes. Core NATS operates on a ‘fire-and-forget’ basis. Meanwhile, JetStream allows you to persist messages to the hard drive, similar to Kafka.

Leveraging the Power of Subject Hierarchies

NATS uses a dot . to create subject hierarchies, making data management very clean. For example: orders.hanoi.created. You can use wildcards for flexible message consumption:

  • orders.hanoi.*: Get all messages regarding orders in Hanoi.
  • orders.>: Get all messages starting with ‘orders’ regardless of the region.

Enabling JetStream to Prevent Data Loss

By default, NATS stores data in RAM. If the server crashes, messages are lost. Create a server.conf file to force it to write to disk:

# server.conf
jetstream {
    store_dir: "/data/nats-jetstream"
    max_mem: 1G
    max_file: 10G
}

# Simple Token Authentication
authorization {
    token: "your-secret-key"
}

Then run the server with the command: nats-server -c server.conf.

Sample Code with Python (nats-py)

Here is how I implement a Producer using Request-Reply—an extremely powerful feature of NATS:

import asyncio
from nats.aio.client import Client as NATS

async def run():
    nc = NATS()
    # Connect with a security token
    await nc.connect("nats://your-secret-key@localhost:4222")

    # Regular message publish
    await nc.publish("updates.itfromzero", b'NATS speed test')
    
    # Request-Reply Pattern: Send and wait for an immediate response
    try:
        res = await nc.request("service.check", b'Status?', timeout=1)
        print(f"Response from server: {res.data.decode()}")
    except asyncio.TimeoutError:
        print("Server is busy, no response received!")

    await nc.close()

if __name__ == '__main__':
    asyncio.run(run())

Monitoring: Don’t Let Your System Run Blind

When going into Production, monitoring is mandatory. NATS supports the nats-top tool, which looks very similar to the top command in Linux but specifically for messages.

# View real-time throughput and connections
nats-top -s localhost:8222

If you use Prometheus, simply add the -m 8222 flag. NATS will automatically expose the /metrics endpoint for you to pull data into Grafana.

Pro tip from the field: Pay attention to Advisory Subjects. NATS will automatically fire warning messages when a Consumer is abruptly disconnected. Thanks to this, I often resolve issues before customers even have time to submit a support ticket.

3 Critical Lessons Learned When Using NATS

  1. Limit the use of Wildcard ‘>’: Don’t subscribe to everything unless absolutely necessary. It will overwhelm your client with too much junk data.
  2. Use Queue Groups for Load Balancing: Want to distribute the load across 5 instances of a service? Use queue groups. NATS will automatically distribute messages in a perfect Round-robin fashion.
  3. Message Size: NATS defaults to a 1MB limit. If you plan to send 50MB files via NATS, stop. It’s best to upload the file to S3 and only send the link via the Broker.

NATS is not a total replacement for Kafka in every Big Data scenario. However, if you prioritize speed, simplicity, and infrastructure cost savings, NATS is definitely the number one candidate today.

Share: