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:
- All preceding messages have been transmitted
- The server has received them
- The server has processed them through its read loop
- 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:
- Critical messages where you need confirmation before proceeding
- End of batch processing
- Before closing a connection
- Measuring RTT (
nc.RTT()uses Flush internally)
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 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:
- 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 is designed to be efficient by default. Flush() is a tool for specific scenarios, not a general-purpose safety net.
-
NATS Protocol - PING/PONG is part of the core NATS protocol, used for both keepalive and synchronization. ↩︎
-
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. ↩︎