NATS doesn’t really have a "max payload" in the way you might think; instead, it has a configurable maximum message size that prevents a single message from consuming all available memory on a publisher, subscriber, or the server itself.

Let’s see NATS in action with a simple publisher and subscriber. First, ensure you have NATS running. A quick way is via Docker:

docker run -p 4222:4222 -p 6222:6222 -p 8222:8222 nats:latest

Now, let’s craft a small Go program for a publisher and a subscriber.

Publisher (pub.go):

package main

import (
	"context"
	"log"
	"time"

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

func main() {
	nc, err := nats.Connect("nats://localhost:4222")
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()

	subject := "my.large.message"
	// Create a large payload (e.g., 1MB)
	payload := make([]byte, 1024*1024) // 1MB

	log.Printf("Publishing message of size %d bytes to subject '%s'", len(payload), subject)
	if err := nc.Publish(subject, payload); err != nil {
		log.Fatalf("Failed to publish message: %v", err)
	}
	nc.Flush() // Ensure the message is sent
	log.Println("Message published successfully.")

	// Give a moment for potential subscriber processing if needed
	time.Sleep(1 * time.Second)
}

Subscriber (sub.go):

package main

import (
	"log"
	"time"

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

func main() {
	nc, err := nats.Connect("nats://localhost:4222")
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()

	subject := "my.large.message"

	sub, err := nc.SubscribeSync(subject)
	if err != nil {
		log.Fatalf("Failed to subscribe to subject '%s': %v", subject, err)
	}
	log.Printf("Subscribed to subject '%s'", subject)

	msg, err := sub.NextMsg(10 * time.Second) // Wait for a message
	if err != nil {
		log.Fatalf("Failed to receive message: %v", err)
	}

	log.Printf("Received message from subject '%s' with payload size %d bytes", msg.Subject, len(msg.Data))
}

Run the subscriber first: go run sub.go Then, run the publisher: go run pub.go

You should see output indicating the message was published and received.

The core of NATS’s message size control lies in the max_payload configuration option. This isn’t a hard limit imposed by the NATS protocol itself, but rather a safeguard set by administrators to prevent resource exhaustion. When a NATS server starts, it reads this configuration. Any message exceeding this size, whether published by a client or forwarded by the server, will be rejected.

The max_payload value is specified in bytes. The default value for max_payload in NATS is 1048576 bytes, which is exactly 1 MiB (1024 * 1024 bytes). This default is often sufficient for many use cases but can be a bottleneck if you need to transmit larger data chunks.

To change this limit, you modify the NATS server configuration file. Here’s a snippet of a nats-server.conf:

listen: 0.0.0.0:4222
max_payload: 10485760 # Set to 10MB

In this example, we’ve increased the max_payload to 10,485,760 bytes (10 MiB). After updating your nats-server.conf file, you’ll need to restart the NATS server for the change to take effect.

If you’re using NATS JetStream, the concept is similar but applied differently. JetStream has its own max_payload setting within its configuration block, which can be distinct from the server’s global max_payload. This allows for finer-grained control over message sizes within JetStream streams.

listen: 0.0.0.0:4222
jetstream: {
  max_payload: 20971520 # Set JetStream max payload to 20MB
}

Here, JetStream is configured to allow messages up to 20 MiB, even if the global server max_payload is set lower. When publishing to a JetStream stream, the system checks against the JetStream max_payload first. If the message exceeds that, it’s rejected. If it’s within the JetStream limit but exceeds the global server max_payload, it’s also rejected. The effective maximum is the lower of the two.

You can check the effective max_payload setting for a running NATS server by querying its monitoring endpoint (if enabled). For example, if your server is running on localhost:8222 for monitoring:

curl http://localhost:8222/varz

This will return a JSON object containing various server statistics and configuration details, including the max_payload value. This is incredibly useful for debugging and verifying your configuration changes.

The max_payload setting is critical for overall system stability. Without it, a single misbehaving publisher could flood the network or consume all available memory on the server or a subscriber, leading to cascading failures. NATS uses this as a simple, effective backpressure mechanism at the individual message level. It’s important to note that NATS is designed for high throughput and low latency, and extremely large messages can begin to work against these core design principles, impacting performance for all clients.

When you increase max_payload, remember that this doesn’t just affect the NATS server; it also implies that clients (publishers and subscribers) must be capable of handling messages of that size in their memory. If a subscriber’s memory is insufficient, it might still fail to process a message even if the NATS server accepted it.

The next logical step after understanding message size limits is to explore how NATS handles message ordering and delivery guarantees, especially within JetStream.

Want structured learning?

Take the full Nats course →