{"record":{"id":"bb6dd217b9b4df0d","repo":"grpc/grpc-java","slug":"unavailable","errorCode":"UNAVAILABLE","errorMessage":"Stream IDs have been exhausted","messagePattern":"Stream IDs have been exhausted","errorType":"error_code","errorClass":"StatusException","httpStatus":null,"severity":"warning","filePath":"netty/src/main/java/io/grpc/netty/NettyClientHandler.java","lineNumber":1063,"sourceCode":"      debugString = \", debug data: \" + new String(debugData, UTF_8);\n    }\n    return statusCode.toStatus()\n        .withDescription(context + \". \" + status.getDescription() + debugString);\n  }\n\n  /**\n   * Gets the client stream associated to the given HTTP/2 stream object.\n   */\n  private NettyClientStream.TransportState clientStream(Http2Stream stream) {\n    return stream == null ? null : (NettyClientStream.TransportState) stream.getProperty(streamKey);\n  }\n\n  private int incrementAndGetNextStreamId() throws StatusException {\n    int nextStreamId = connection().local().incrementAndGetNextStreamId();\n    if (nextStreamId < 0) {\n      logger.fine(\"Stream IDs have been exhausted for this connection. \"\n              + \"Initiating graceful shutdown of the connection.\");\n      throw EXHAUSTED_STREAMS_STATUS.asException();\n    }\n    return nextStreamId;\n  }\n\n  private Http2Stream requireHttp2Stream(int streamId) {\n    Http2Stream stream = connection().stream(streamId);\n    if (stream == null) {\n      // This should never happen.\n      throw new AssertionError(\"Stream does not exist: \" + streamId);\n    }\n    return stream;\n  }\n\n  private class FrameListener extends Http2FrameAdapter {\n    private boolean firstSettings = true;\n\n    @Override\n    public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {","sourceCodeStart":1045,"sourceCodeEnd":1081,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/netty/src/main/java/io/grpc/netty/NettyClientHandler.java#L1045-L1081","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","If you see it surface, retry the RPC with backoff (it is retryable UNAVAILABLE status).","Ensure retry policy/transparent retries are not disabled for the method.","Reduce single-connection stream lifetime pressure: use multiple channels or let server send periodic GOAWAYs so connections are recycled before stream-ID exhaustion."],"exampleFix":"// wrap gRPC calls with retry on UNAVAILABLE\nManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)\n    .enableRetry()\n    .maxRetryAttempts(5)\n    .build();\n// retryingStub will transparently re-issue RPCs that fail with UNAVAILABLE,\n// which also covers stream-ID exhaustion while the connection is replaced.\nGreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel)\n    .withRetry(\n        RetrySettings.newBuilder()\n            .addRetryableCode(Status.Code.UNAVAILABLE)\n            .build());","handlingStrategy":"retry","validationCode":"long streamsOpened = channelStats.activeStreams(); // track per-connection stream count in metrics\nif (streamsOpened > 2_000_000_000L) { logger.warn(\"Approaching HTTP/2 stream ID exhaustion; expect connection recycle\"); }","typeGuard":null,"tryCatchPattern":"try {\n  return blockingStub.call(request);\n} catch (StatusRuntimeException e) {\n  if (e.getStatus().getCode() == Status.Code.UNAVAILABLE) {\n    return retryWithBackoff(request, 5);\n  }\n  throw e;\n}","preventionTips":["Enable gRPC retries / transparent retry for UNAVAILABLE on high-QPS methods.","Keep long-lived channels but rely on the handler's graceful shutdown; do not disable connection replacement.","Have servers send periodic GOAWAYs to recycle connections before stream IDs exhaust.","Monitor per-connection stream counts in metrics for very high-traffic clients."],"tags":["grpc","netty","http2","stream-id","unavailable"],"backgroundTag":"resource-exhausted","analyzedSha":"64daddc1f3d1975670f769f3e97bde8b2ba32d25","analyzedAt":"2026-09-08T06:14:57.704Z","contentChangedAt":"2026-09-08T06:14:57.704Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}