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
- 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.
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
- 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.
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
- TsiHandshakeHandler encountered exception
- Malformed status code
- Could not find Jetty NPN/ALPN or Conscrypt as installed JDK
- Unsupported provider: ${provider}
- Could not find TLS ALPN provider; no working netty-tcnative,
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/bb6dd217b9b4df0d.
Report an issue: GitHub.