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

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.

With ~1ms network RTT, this creates roughly a 100x throughput 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("events", data)
    nc.Flush()  // 100,000 round-trips
}

// Pattern B: Batch flush (~100,000 msg/sec)
for i := 0; i < 100000; i++ {
    nc.Publish("events", 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 write buffer (32KB by default) on a regular signal from publish operations.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. For graceful shutdown, prefer Drain() over Close() – it flushes pending messages and unsubscribes cleanly.

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 handles buffering automatically. Flush() is for synchronization, not routine use.


  1. NATS Protocol - PING/PONG is defined in the core protocol for keepalive. Client libraries use it for synchronization by sending a PING and blocking until the PONG returns. See also the nats.go source↩︎

  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. ↩︎