Bidirectional streaming is a request/response model where both sides send a stream of messages independently. Unlike a single request followed by a single response, both client and server can read and write at any time. gRPC implements this on top of HTTP/2 streams, and once you understand the mechanics it is one of the most powerful tools for real-time systems.

Advertisement

Why bidirectional?

Chat, multiplayer games, real-time analytics dashboards, voice/video transport — anywhere both ends need to push events to the other without polling. The alternative (long-poll over HTTP/1.1) wastes connections and adds latency. Bidi over HTTP/2 multiplexes one TCP connection across many streams, so you avoid head-of-line blocking and keep the round-trip latency low.

The wire-level mechanics

HTTP/2 frames let the client and server send DATA frames in either direction on the same stream ID. gRPC uses the Length-Prefixed-Message format inside each DATA frame: 1-byte compressed flag, 4-byte big-endian length, then the protobuf payload. The stream stays open until either side sends END_STREAM.

Advertisement

Server (Python) sample

import grpc, chat_pb2, chat_pb2_grpc
from concurrent import futures

class ChatService(chat_pb2_grpc.ChatServicer):
    def Chat(self, request_iterator, context):
        for msg in request_iterator:           # read inbound
            yield chat_pb2.Echo(text=f'<<{msg.text}>>')  # write outbound

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
chat_pb2_grpc.add_ChatServicer_to_server(ChatService(), server)
server.add_insecure_port('[::]:50051')
server.start(); server.wait_for_termination()

Client sample

import grpc, chat_pb2, chat_pb2_grpc, threading

channel = grpc.insecure_channel('localhost:50051')
stub = chat_pb2_grpc.ChatStub(channel)

def gen():
    for line in iter(input, ''):
        yield chat_pb2.Msg(text=line)

for echo in stub.Chat(gen()):     # async iter both ways
    print('server:', echo.text)

Backpressure + flow control

HTTP/2 flow control is per-stream and per-connection. If the consumer doesn't read, WINDOW_UPDATE frames stop arriving and the producer blocks. Tune initialWindowSize if you push large messages. On the application layer, gRPC adds a per-call deadline — set it; don't rely on TCP keepalive alone.

When NOT to use bidi

If 95% of your traffic is request/response, stick with unary calls — they are cheaper to reason about. Bidi shines when the message rate is independent on both sides, not just bidirectional in the request-response sense.

Bidi over HTTP/2 = one TCP connection, independent message streams in both directions, with flow control built in. Use it when both sides drive events.