What Flush() Actually Does in NATS
Most developers assume Flush() empties a local buffer. I’ve seen this confusion cause real performance problems: people call Flush after every publish and wonder why their throughput is terrible.
In NATS, Flush does much more than empty a buffer.
When you call Flush(), the client writes a PING directly to the socket under the connection lock (bypassing the background flusher goroutine) and waits for a PONG response.1 Because NATS processes commands in order on the connection, receiving the PONG confirms that the server has parsed every message sent before the PING.
This is protocol-level synchronization, not buffer management.
What Flush Guarantees
When Flush() returns successfully:
- All preceding messages have been transmitted
- The server has received them
- The server has parsed them through its read loop (internal subscriber delivery may still be in flight)
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 same-host or LAN environment, that’s 50-500 microseconds. Same-AZ in a cloud region typically adds 1-3 ms; cross-AZ or cross-region pushes it to 10-100 ms or more. Across a WAN, it’s whatever your RTT is.
With ~1ms network RTT, this creates a large throughput difference – easily 10x to 100x or more depending on RTT, message size, and write coalescing – 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:
- Critical messages where you need confirmation before proceeding
- End of batch processing
- Before closing a connection
- Measuring round-trip time (
nc.RTT()uses the same PING/PONG mechanism, with a 10-second timeout; it returns wall-clock time for the full round-trip, which includes server queuing – not a raw network RTT measurement)
Avoid Flush in:
- Hot paths where latency matters
- Per-message confirmation (use JetStream instead)
- Fire-and-forget scenarios (the background flusher handles this)
The Background Flusher
NATS clients run a background goroutine that flushes the write buffer (32KB by default) on demand.3 Every Publish call sends a non-blocking signal on a buffered channel (capacity 1); the goroutine wakes, acquires the connection lock, and flushes whatever has accumulated. Because the channel is size 1, concurrent publishes coalesce – many publishes during a flush produce at most one additional flush. For most fire-and-forget publishing you don’t need explicit flushes at all.
One edge case: at very low publish rates – particularly a single publish followed immediately by Close() – the flusher goroutine may not run before the connection closes. Call Flush() or Drain() before exiting to make sure the buffer is drained.
Explicit Flush() is for when you need the confirmation, not just the transmission. The client also provides FlushTimeout() and FlushWithContext() for deadline-aware code.
Graceful Shutdown
For graceful shutdown, prefer Drain() over Close() – it flushes pending messages and unsubscribes cleanly. Drain() returns immediately and runs asynchronously, so to know when it is safe to exit, set a ClosedCB on the connection options and block until that callback fires.
Practical Guidance
If you’re calling Flush() after every publish, you’re probably doing it wrong. Either:
- You don’t need confirmation – remove the Flush calls and let the background flusher handle it
- You need delivery guarantees – use JetStream, which provides acknowledgments at the stream level
- You need batched confirmation – accumulate messages and flush periodically
The NATS client handles buffering automatically. Flush() is for synchronization, not routine use.
-
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. ↩︎
-
JetStream - JetStream provides acknowledgment-based delivery guarantees on top of Core NATS. ↩︎
-
NATS Go Client - The default buffer size is 32KB, configurable via connection options. ↩︎