NIO
data handling
I/O performance
write-read imbalance
data throughput

NIO fail when writing more data than reading

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Java NIO, writing faster than the peer can read does not usually mean the API is broken. It means your code is hitting backpressure. A non-blocking SocketChannel.write call may write only part of the buffer, or even zero bytes, and if you ignore that fact and keep producing data, your outbound state eventually becomes inconsistent or unbounded.

Partial Writes Are Normal In Non-Blocking I/O

A core NIO rule is that readiness is not the same as completion. Even when a channel is writable, a single call may not flush the whole message.

java
int written = channel.write(buffer);
System.out.println("bytes written = " + written);

If buffer still has remaining bytes after the call, you must keep that buffer and continue later. Throwing it away or assuming everything was sent is a bug.

Why Writing Can Outrun Reading

TCP already has flow control, but at the application level you still need to handle it correctly. If the remote side reads slowly:

  • kernel send buffers fill up
  • 'write begins returning smaller counts'
  • eventually write may return 0 in non-blocking mode

At that point, your application has to stop pretending the data is gone.

A Correct Outbound Queue Pattern

A common NIO server pattern keeps a per-connection queue of pending outbound ByteBuffer objects.

java
1Queue<ByteBuffer> pendingWrites = new ArrayDeque<>();
2
3void enqueueMessage(byte[] payload) {
4    pendingWrites.add(ByteBuffer.wrap(payload));
5}
6
7void flush(SocketChannel channel) throws IOException {
8    while (!pendingWrites.isEmpty()) {
9        ByteBuffer buffer = pendingWrites.peek();
10        channel.write(buffer);
11        if (buffer.hasRemaining()) {
12            break;
13        }
14        pendingWrites.remove();
15    }
16}

This pattern keeps partially written buffers until they are fully drained.

Register OP_WRITE Only When Needed

In selector-based NIO, OP_WRITE should usually be enabled only when there is pending data to flush.

java
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);

Then, after the queue becomes empty, remove OP_WRITE interest again.

java
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);

If you leave OP_WRITE enabled all the time, selectors can wake up continuously because sockets are often considered writable most of the time.

Do Not Allocate Infinite Outbound Memory

If producers can enqueue messages faster than the network can send them, an unbounded queue becomes a memory leak under load. Real systems need a policy such as:

  • backpressure to the producer
  • dropping low-priority messages
  • disconnecting slow clients
  • bounding the queue per connection

NIO itself will not choose that policy for you.

Reads And Writes Are Logically Separate

Another common misunderstanding is expecting writes to succeed because reads are also happening on the same connection. Reading inbound data does not magically free your outbound pressure problem. Each direction has its own buffers and flow.

That is why a server can read perfectly fine while writes stall, or vice versa.

A Minimal Selector Sketch

java
1if (key.isWritable()) {
2    SocketChannel channel = (SocketChannel) key.channel();
3    flush(channel);
4    if (pendingWrites.isEmpty()) {
5        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
6    }
7}

The important behavior is not the exact boilerplate. It is the discipline of keeping unsent bytes until the channel can accept more.

Common Pitfalls

The most common mistake is assuming channel.write(buffer) sends the whole message. Another is discarding or reusing a buffer even though it still has remaining bytes. Developers also often leave OP_WRITE permanently enabled, causing wasteful selector wakeups. Finally, if outbound queues are unbounded, a slow reader on the other side can turn one connection into a memory pressure problem for the whole server.

Summary

  • In non-blocking NIO, partial writes are normal and must be handled explicitly.
  • Writing faster than the peer reads creates backpressure, not an API failure.
  • Keep partially written buffers in a per-connection outbound queue.
  • Enable OP_WRITE only when there is pending data to flush.
  • Bound your write queues or apply a clear slow-consumer policy.

Course illustration
Course illustration

All Rights Reserved.