Console Login

NATS JetStream: Architecting Low-Latency Event Loops for Nordic Microservices

NATS JetStream: Architecting Low-Latency Event Loops for Nordic Microservices

Let’s be honest for a second: running Apache Kafka for a startup or a mid-sized microservices cluster is like using a sledgehammer to crack a nut. A very heavy, Java-based sledgehammer that eats RAM for breakfast and requires a dedicated ZooKeeper team just to stay upright. I have spent too many nights debugging JVM garbage collection pauses when I should have been sleeping.

If you are building cloud-native applications in 2021, you don't always need the "enterprise" weight of Kafka or the routing complexity of RabbitMQ. You need raw speed, simplicity, and persistence that doesn't require a PhD to configure.

Enter NATS JetStream. With the recent deprecation of NATS Streaming (STAN), JetStream is the new standard built directly into the NATS binary. It offers persistence, at-least-once delivery, and horizontal scalability, all within a binary that is less than 20MB.

This guide dives into deploying a production-ready NATS JetStream cluster, optimized for the low-latency requirements of the Nordic market and the strict data sovereignty laws we face here in Norway.

The Latency Problem: Why Location Matters

Before we touch the config files, let's talk physics. You can optimize your Go binary code all you want, but if your message broker is hosted in Frankfurt and your users are in Oslo, you are fighting a losing battle against the speed of light. Round-trip time (RTT) matters.

Pro Tip: In a Request-Reply pattern, network latency is paid twice (Request + Reply). Hosting your NATS cluster on CoolVDS infrastructure in Oslo keeps your internal RTT typically under 2-3ms for local services, compared to 25-30ms routing down to Central Europe. This adds up when you have chained microservices.

Architecture: NATS JetStream vs. The World

Why are we shifting to NATS in late 2021? It comes down to resource efficiency and operational simplicity. Here is how it stacks up against the usual suspects on a standard 2 vCPU / 4GB RAM VPS:

Feature NATS JetStream Apache Kafka RabbitMQ
Binary Size ~15MB (Go) ~500MB+ (JVM) ~100MB+ (Erlang)
Startup Time < 1 Second 30+ Seconds 5-10 Seconds
Ops Complexity Single Binary ZooKeeper + Broker Erlang Runtime
Persistence File/Memory (JetStream) Log-based Queue-based

Step 1: The Server Configuration

We will configure a three-node cluster for high availability. In a production environment on CoolVDS, you would place these on three separate instances to survive a node failure. JetStream uses the RAFT consensus algorithm, so we need an odd number of servers to maintain a quorum.

Here is a hardened nats-server.conf designed for 2021-era security standards (TLS 1.3) and performance:

# /etc/nats/nats-server.conf

server_name: nats-01
listen: 0.0.0.0:4222

# Enable JetStream
jetstream {
    store_dir: "/data/nats/jetstream"
    max_memory_store: 2G
    max_file_store: 100G
}

# Cluster Definition
cluster {
    name: "coolvds-cluster"
    listen: 0.0.0.0:6222
    
    # Route connections to other nodes
    routes: [
        nats://nats-01:6222
        nats://nats-02:6222
        nats://nats-03:6222
    ]
}

# Security: We don't want the world reading our streams
authorization {
    token: "s3cr3t_t0k3n_change_me"
}

# System tuning for high loads
max_connections: 10000
max_payload: 2MB
write_deadline: "2s"

Critical Ops Note: The store_dir is where the data lives. On CoolVDS, we strictly use NVMe storage. JetStream writes sequential logs to disk. If you use standard spinning rust (HDD) or low-tier SATA SSDs, your I/O wait (iowait) will skyrocket, and the RAFT consensus will time out, causing leader elections loops. Don't be cheap on disk IOPS.

Step 2: Deploying via Docker Compose

For a quick dev environment or a single-node production test, Docker is the standard. We are using the alpine image to keep the attack surface minimal.

version: "3.8"
services:
  nats:
    image: nats:2.6-alpine
    container_name: nats-server
    ports:
      - "4222:4222"
      - "8222:8222"
    volumes:
      - ./nats.conf:/etc/nats/nats-server.conf
      - ./data:/data/nats/jetstream
    command: "-c /etc/nats/nats-server.conf"
    restart: always
    ulimits:
      nofile:
        soft: 65535
        hard: 65535

Notice the ulimits. NATS handles massive concurrency. The default Linux file descriptor limit (often 1024) is a joke for a message bus. We bump this to 65535 to ensure we don't drop connections during traffic spikes.

Step 3: The Go Client Implementation

NATS is written in Go, and the Go client is a first-class citizen. Here is how you connect to the JetStream context and publish a persistent message. This code assumes you are using the github.com/nats-io/nats.go library (v1.13.0 is stable as of late 2021).

Publisher Code

package main

import (
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	// Connect to the CoolVDS NATS instance
	nc, err := nats.Connect("nats://10.0.0.5:4222", nats.Token("s3cr3t_t0k3n_change_me"))
	if err != nil {
		log.Fatal(err)
	}
	defer nc.Close()

	// Create JetStream Context
	js, err := nc.JetStream()
	if err != nil {
		log.Fatal(err)
	}

	// Define a Stream (Idempotent)
	_, err = js.AddStream(&nats.StreamConfig{
		Name:     "ORDERS",
		Subjects: []string{"ORDERS.*"},
		Storage:  nats.FileStorage,
		Replicas: 1, // Use 3 for clustered setups
	})
	if err != nil {
		log.Printf("Stream setup: %v", err)
	}

	// Publish a persistent message
	_, err = js.Publish("ORDERS.created", []byte(`{"id": 101, "item": "NVMe-VPS"}`))
	if err != nil {
		log.Fatal(err)
	} else {
		log.Println("Order published successfully to JetStream")
	}
}

Consumer Code (Pull Subscription)

Pull consumers are generally preferred over Push consumers in modern NATS architectures because they allow the client to control the flow rate (backpressure), preventing the application from being overwhelmed.

sub, err := js.PullSubscribe("ORDERS.created", "order-processor")
if err != nil {
    log.Fatal(err)
}

// Fetch batch of 10 messages
ms, err := sub.Fetch(10, nats.MaxWait(time.Second))
if err != nil {
    log.Println("No new orders...")
    return
}

for _, msg := range ms {
    log.Printf("Processing Order: %s", string(msg.Data))
    msg.Ack() // Crucial: Acknowledge execution so it's removed from pending
}

Data Sovereignty and Schrems II

Since the Schrems II ruling in 2020, relying on US-based cloud providers for handling European user data has become a legal minefield. The Norwegian Data Protection Authority (Datatilsynet) has been very clear about the risks of data transfers.

When you deploy NATS on CoolVDS, you are keeping the data stream physically located in Norway. The persistence files generated by JetStream remain on local NVMe drives within our Oslo data center. This greatly simplifies your GDPR compliance strategy compared to piping data through a managed queue service hosted by a hyperscaler with US ties.

Performance Tuning for Linux

Before you go live, you need to tweak the host OS. The default sysctl settings on most Linux distros are tuned for general desktop usage, not high-throughput messaging.

Add these to /etc/sysctl.conf:

# Increase system file descriptor limit
fs.file-max = 100000

# TCP Buffer tuning for 10Gbps+ networks
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# Congestion control (BBR is available in kernel 4.9+)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

We enable TCP BBR on all standard CoolVDS templates because it handles packet loss and throughput significantly better than the old CUBIC algorithm, especially over public internet connections.

Conclusion

NATS JetStream represents the maturation of the Go ecosystem. It strips away the complexity of legacy enterprise brokers while keeping the reliability we need for financial and e-commerce transactions.

However, software is only half the equation. A message broker is I/O bound. It lives and dies by the speed of the disk and the latency of the network. Don't cripple your architecture by hosting it on oversold hardware or across an ocean.

If you are ready to build the next generation of Norwegian services, you need infrastructure that respects the physics of latency.

Deploy a high-performance Ubuntu 20.04 instance on CoolVDS today and see what sub-millisecond latency feels like.