public ChannelOutput sendAndReceive(Send request) throws IOException {
    Channel streamChannel;
    try {
      streamChannel =
          channelPool.acquire().get(http2ClientConfig.http2BlockingChannelAcquireTimeoutMs, TimeUnit.MILLISECONDS);
    } catch (Exception e) {
      throw new IOException("Can't acquire stream channel from " + getRemoteHost() + ":" + getRemotePort(), e);
    }

    CompletableFuture<ByteBuf> responsePromise = new CompletableFuture<ByteBuf>();
    streamChannel.attr(RESPONSE_PROMISE).set(responsePromise);
    streamChannel.attr(CHANNEL_POOL_ATTRIBUTE_KEY).set(channelPool);
    boolean success = streamChannel.writeAndFlush(request)
        .awaitUninterruptibly(http2ClientConfig.http2BlockingChannelSendTimeoutMs, TimeUnit.MILLISECONDS);
    if (!success) {
      if (streamChannel.attr(RESPONSE_PROMISE).getAndSet(null) != null) {
        channelPool.release(streamChannel);
      }
      throw new IOException(
          "Failed to write and flush request on time, from " + getRemoteHost() + ":" + getRemotePort());
    }

    ByteBuf responseByteBuf;
    try {
      responseByteBuf =
          responsePromise.get(http2ClientConfig.http2BlockingChannelReceiveTimeoutMs, TimeUnit.MILLISECONDS);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
      if (streamChannel.attr(RESPONSE_PROMISE).getAndSet(null) != null) {
        channelPool.release(streamChannel);
      }
      throw new IOException("Failed to receive response from " + getRemoteHost() + ":" + getRemotePort(), e);
    }
    NettyByteBufDataInputStream dataInputStream = new NettyByteBufDataInputStream(responseByteBuf);
    // Because readLong() is called to get entire stream size(long),
    // the size of remaining data should be dataInputStream.readLong() - 8
    return new ChannelOutput(dataInputStream, dataInputStream.readLong() - Long.BYTES);
  }
A new stream channel is created for each send.
ambry-network/src/main/java/com/github/ambry/network/http2/Http2BlockingChannel.java