John Weldon

What Flush() Actually Does in NATS

Most developers assume Flush() empties a local buffer. I’ve seen this confusion cause real performance problems - people calling Flush after every publish, wondering why their throughput is terrible.

In NATS, Flush does much more than empty a buffer.

When you call Flush(), the client sends a PING to the server and waits for a PONG response.1 Because NATS processes commands in order, receiving the PONG confirms that the server has received and processed all messages sent before the PING.

This is protocol-level synchronization, not buffer management.

What Flush Guarantees

When Flush() returns successfully:

  1. All preceding messages have been transmitted
  2. The server has received them
  3. The server has processed them through its read loop
  4. Message ordering has been preserved

What Flush Does Not Guarantee

Flush confirms server receipt, not subscriber delivery. The server may have received your message, but subscribers might not have processed it yet. For subscriber acknowledgment, you need JetStream2 or request-reply patterns.

Why This Matters

The round-trip requirement means each Flush() costs network latency. In a typical LAN environment, that’s 50-500 microseconds. Across a WAN, it’s whatever your RTT is.

This creates a 100x performance difference between these two patterns:

// Note: Error handling omitted for clarity

// Pattern A: Flush per message (~1,000 msg/sec)
for i := 0; i < 100000; i++ {
    nc.Publish("topic", data)
    nc.Flush()  // 100,000 round-trips
}

// Pattern B: Batch flush (~100,000 msg/sec)
for i := 0; i < 100000; i++ {
    nc.Publish("topic", data)
}
nc.Flush()  // 1 round-trip

When to Use Flush

Use Flush for:

Avoid Flush in:

The Background Flusher

NATS clients run a background goroutine that flushes the buffer when it reaches capacity (32KB by default) or when writes would otherwise block.3 For most fire-and-forget publishing, you don’t need explicit flushes at all. The automatic flushing keeps messages flowing without blocking your code.

Explicit Flush() is for when you need the confirmation, not just the transmission. The client also provides FlushTimeout() and FlushWithContext() for deadline-aware code.

Practical Guidance

If you’re calling Flush() after every publish, you’re probably doing it wrong. Either:

  1. You don’t need confirmation - remove the Flush calls and let the background flusher handle it
  2. You need delivery guarantees - use JetStream, which provides acknowledgments at the stream level
  3. You need batched confirmation - accumulate messages and flush periodically

The NATS client is designed to be efficient by default. Flush() is a tool for specific scenarios, not a general-purpose safety net.


  1. NATS Protocol - PING/PONG is part of the core NATS protocol, used for both keepalive and synchronization. ↩︎

  2. JetStream - JetStream provides acknowledgment-based delivery guarantees on top of Core NATS. ↩︎

  3. NATS Go Client - The default buffer size is 32KB, configurable via connection options. ↩︎