netty/netty · critical · IllegalReferenceCountException

refCnt: 0

Error message

refCnt: 0

What it means

Thrown by AbstractByteBuf.ensureAccessible as IllegalReferenceCountException(0) when any buffer-accessing method is called on a buffer whose reference count is zero (it has been release()d). ensureAccessible is the universal guard called at the top of nearly every accessor, gated by the io.netty.buffer.checkAccessible flag (default true). A zero count means the buffer's memory may already be returned to its pool, so all further access is forbidden as a use-after-free defense.

Source

Thrown at buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java:1480

        }
    }

    private void checkReadableBytes0(int minimumReadableBytes) {
        ensureAccessible();
        if (checkBounds && readerIndex > writerIndex - minimumReadableBytes) {
            throw new IndexOutOfBoundsException(String.format(
                    "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
                    readerIndex, minimumReadableBytes, writerIndex, this));
        }
    }

    /**
     * Should be called by every method that tries to access the buffers content to check
     * if the buffer was released before.
     */
    protected final void ensureAccessible() {
        if (checkAccessible && !isAccessible()) {
            throw new IllegalReferenceCountException(0);
        }
    }

    final void setIndex0(int readerIndex, int writerIndex) {
        this.readerIndex = readerIndex;
        this.writerIndex = writerIndex;
    }

    final void discardMarks() {
        markedReaderIndex = markedWriterIndex = 0;
    }

    /**
     * Obtain the memory address without checking {@link #ensureAccessible()} first, if possible.
     */
    long _memoryAddress() {
        return isAccessible() && hasMemoryAddress() ? memoryAddress() : 0L;
    }

View on GitHub (pinned to 70040aacae)

Solutions

  1. Call buf.retain() before handing a buffer to code that will release it, if you still need it afterward; release it in your own finally too.
  2. Track ownership explicitly: assign one owner responsible for release(); use try-with-resources via a wrapper or ReferenceCounted semantics.
  3. Add buf.refCnt() / ByteBufUtil.isAccessible(buf) assertions in debug to catch double releases early.
  4. Avoid releasing buffers you did not retain (Netty releases inbound buffers after the pipeline by default).

Example fix

// before
buf.release();
ctx.writeAndFlush(buf); // downstream releases again, then:
int x = buf.readInt(); // refCnt 0 -> IllegalReferenceCountException

// after
buf.retain();
ctx.writeAndFlush(buf);
// ... later, when truly done:
buf.release();
Defensive patterns

Strategy: validation

Validate before calling

// Check accessibility before any access
if (!buf.isAccessible()) {
    // re-acquire or skip; do NOT call any getter on a released buffer
    return;
}
int v = buf.readInt();

Type guard

static boolean isAlive(ByteBuf buf) {
    return buf.refCnt() > 0;
}

Try / catch

try {
    buf.writeInt(v);
} catch (IllegalReferenceCountException e) {
    buf = alloc.buffer(); // re-acquire a live buffer
    buf.writeInt(v);
}

Prevention

When it happens

Trigger: Calling any getter/reader/writer (or toString, release again) on a buffer after release() was called once too many times, or after a downstream handler released it (e.g. ctx.writeAndFlush released the buffer, then you touch it).

Common situations: Forgetting that writeAndFlush/ChannelPipeline release outbound buffers; releasing a buffer in a finally block AND in a downstream handler (double release); passing a buffer to a codec that retains/releases it while you still hold a reference; autoRead/ref-count bugs in pooled allocators.

Related errors


AI-assisted analysis of netty/netty@70040aacae (2026-08-14). Data as JSON: /api/errors/f582781726884e49. Report an issue: GitHub.