grpc/grpc-java · warning · StatusException

UNAVAILABLE

UNAVAILABLE

Error message

Stream IDs have been exhausted

What it means

HTTP/2 stream IDs are signed 31-bit integers; each new stream on a connection increments the local stream ID. When the ID overflows past Integer.MAX_VALUE, the Netty gRPC client throws a StatusException with Status UNAVAILABLE, message 'Stream IDs have been exhausted', which triggers a graceful shutdown of the connection so a new one (restarting stream IDs) can be created. It is thrown by NettyClientHandler.incrementAndGetNextStreamId when Http2Connection.LocalFlow/local().incrementAndGetNextStreamId() returns a negative value.

Source

Thrown at netty/src/main/java/io/grpc/netty/NettyClientHandler.java:1063

      debugString = ", debug data: " + new String(debugData, UTF_8);
    }
    return statusCode.toStatus()
        .withDescription(context + ". " + status.getDescription() + debugString);
  }

  /**
   * Gets the client stream associated to the given HTTP/2 stream object.
   */
  private NettyClientStream.TransportState clientStream(Http2Stream stream) {
    return stream == null ? null : (NettyClientStream.TransportState) stream.getProperty(streamKey);
  }

  private int incrementAndGetNextStreamId() throws StatusException {
    int nextStreamId = connection().local().incrementAndGetNextStreamId();
    if (nextStreamId < 0) {
      logger.fine("Stream IDs have been exhausted for this connection. "
              + "Initiating graceful shutdown of the connection.");
      throw EXHAUSTED_STREAMS_STATUS.asException();
    }
    return nextStreamId;
  }

  private Http2Stream requireHttp2Stream(int streamId) {
    Http2Stream stream = connection().stream(streamId);
    if (stream == null) {
      // This should never happen.
      throw new AssertionError("Stream does not exist: " + streamId);
    }
    return stream;
  }

  private class FrameListener extends Http2FrameAdapter {
    private boolean firstSettings = true;

    @Override
    public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. This is handled internally: the handler initiates a graceful connection shutdown and new RPCs are transparently retried/dialed on a new connection — usually no user action needed; just treat the RPC as UNAVAILABLE and retry.
  2. If you see it surface, retry the RPC with backoff (it is retryable UNAVAILABLE status).
  3. Ensure retry policy/transparent retries are not disabled for the method.
  4. Reduce single-connection stream lifetime pressure: use multiple channels or let server send periodic GOAWAYs so connections are recycled before stream-ID exhaustion.

Example fix

// wrap gRPC calls with retry on UNAVAILABLE
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
    .enableRetry()
    .maxRetryAttempts(5)
    .build();
// retryingStub will transparently re-issue RPCs that fail with UNAVAILABLE,
// which also covers stream-ID exhaustion while the connection is replaced.
GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel)
    .withRetry(
        RetrySettings.newBuilder()
            .addRetryableCode(Status.Code.UNAVAILABLE)
            .build());
Defensive patterns

Strategy: retry

Validate before calling

long streamsOpened = channelStats.activeStreams(); // track per-connection stream count in metrics
if (streamsOpened > 2_000_000_000L) { logger.warn("Approaching HTTP/2 stream ID exhaustion; expect connection recycle"); }

Try / catch

try {
  return blockingStub.call(request);
} catch (StatusRuntimeException e) {
  if (e.getStatus().getCode() == Status.Code.UNAVAILABLE) {
    return retryWithBackoff(request, 5);
  }
  throw e;
}

Prevention

When it happens

Trigger: A single long-lived client connection that has successfully opened more than 2^31 (~2.1 billion) streams; typical with high-QPS traffic pinned to one HTTP/2 connection (e.g. no connection churn, keep-alive, or channel reuse across billions of requests).

Common situations: High-throughput microservices behind one gRPC channel hitting a server for days/weeks without connection recycling; load tests generating billions of RPCs over one channel; server-side settings that never force connection shutdown (GOAWAY) so the client never re-connects.

Related errors


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/bb6dd217b9b4df0d. Report an issue: GitHub.