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 A Publish call signals it through a buffered channel (capacity 1), skipping the signal when one is already pending; 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. If a publish fills the buffer before the flusher runs, the publishing goroutine flushes it inline. For most fire-and-forget publishing you don’t need explicit flushes at all.
Close() also flushes the buffer before closing the socket, so a publish followed immediately by Close() is still written out. What Close() does not give you is confirmation that the server received it, and if the connection is mid-reconnect, anything buffered is discarded. Call Flush() before exiting when you need that confirmation, or use Drain() for a graceful shutdown.
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. Don’t call Close() after Drain() returns; that cuts the drain short.
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
FlushTimeout()in 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 with
nats.WriteBufferSize(). ↩︎