Published on

gRPC Streaming in 2026 — Server Streaming, Client Streaming, and Bidirectional

Authors

Introduction

HTTP/1.1 and REST force request-response cycles. gRPC runs on HTTP/2 with multiplexing. Add streaming, and you get bidirectional communication that's faster, leaner, and more powerful than REST.

Three streaming modes: server-streaming (real-time data), client-streaming (bulk uploads), bidirectional (collaborative features). Master all three and you'll build faster, more responsive backends.

gRPC Streaming Types

Unary (RPC): Client sends one message, server responds with one. Like HTTP POST.

Server-streaming: Client sends one message, server responds with multiple messages over time.

Client-streaming: Client sends multiple messages, server responds once with a summary.

Bidirectional-streaming: Both send multiple messages, simultaneously, over one connection.

Each type solves different problems. Choose based on your use case.

Defining Streaming RPCs in Protobuf

Define your service in .proto:

syntax = "proto3";

package stock;

message StockRequest {
  string symbol = 1;
}

message StockPrice {
  string symbol = 1;
  float price = 2;
  int64 timestamp = 3;
}

message FileChunk {
  bytes data = 1;
  int32 sequence = 2;
}

message FileUploadRequest {
  string filename = 1;
  FileChunk chunk = 2;
}

message UploadResponse {
  string filename = 1;
  int64 size = 2;
  bool success = 3;
}

message ChatMessage {
  string sender = 1;
  string text = 2;
  int64 timestamp = 3;
}

service StockService {
  // Server-streaming: get live stock prices
  rpc StreamPrices(StockRequest) returns (stream StockPrice);

  // Client-streaming: upload a file in chunks
  rpc UploadFile(stream FileUploadRequest) returns (UploadResponse);

  // Bidirectional: chat with backpressure
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

Generate TypeScript code:

protoc --plugin=./node_modules/.bin/grpc_tools_node_protoc_plugin \
  --js_out=import_style=commonjs,binary:generated \
  --grpc_out=grpc_js:generated \
  stock.proto

Node.js @grpc/grpc-js Streaming Implementation

Install dependencies:

npm install @grpc/grpc-js @grpc/proto-loader

Server implementation:

import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';

const PROTO_PATH = './stock.proto';

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const stockProto = grpc.loadPackageDefinition(packageDefinition);
const StockService = stockProto.stock.StockService;

// Server-streaming implementation
function streamPrices(call: any) {
  const symbol = call.request.symbol;

  // Simulate stock price updates
  const interval = setInterval(() => {
    const price = Math.random() * 100 + 50;
    call.write({
      symbol,
      price,
      timestamp: Date.now()
    });
  }, 1000);

  call.on('cancelled', () => {
    clearInterval(interval);
  });

  // Explicit end (optional; server can stream indefinitely)
  setTimeout(() => {
    call.end();
  }, 10000);
}

const server = new grpc.Server();
server.addService(StockService, { streamPrices });
server.bindAsync('127.0.0.1:50051', grpc.ServerCredentials.createInsecure(), () => {
  server.start();
  console.log('Server running at http://127.0.0.1:50051');
});

Client:

import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';

const PROTO_PATH = './stock.proto';

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const stockProto = grpc.loadPackageDefinition(packageDefinition);
const StockService = stockProto.stock.StockService;

const client = new StockService(
  '127.0.0.1:50051',
  grpc.credentials.createInsecure()
);

const call = client.streamPrices({ symbol: 'AAPL' });

call.on('data', (response: any) => {
  console.log(`${response.symbol}: $${response.price}`);
});

call.on('end', () => {
  console.log('Stream ended');
});

call.on('error', (error: any) => {
  console.error('Stream error:', error);
});

Server-Streaming for Real-Time Data (Stock Prices, Logs)

Stream stock price updates:

function streamPrices(call: any) {
  const symbol = call.request.symbol;

  const priceStream = priceDatabase.subscribe(symbol);

  priceStream.on('price', (price: number) => {
    call.write({
      symbol,
      price,
      timestamp: Date.now()
    });
  });

  call.on('cancelled', () => {
    priceStream.unsubscribe();
  });
}

Stream logs:

function streamLogs(call: any) {
  const logFile = call.request.filename;

  const tailProcess = spawn('tail', ['-f', logFile]);

  tailProcess.stdout.on('data', (data: Buffer) => {
    call.write({
      line: data.toString(),
      timestamp: Date.now()
    });
  });

  call.on('cancelled', () => {
    tailProcess.kill();
  });
}

Server-streaming is perfect for dashboards, metrics, and live data feeds.

Client-Streaming for File Upload

Receive a file in chunks:

function uploadFile(call: any, callback: any) {
  let totalSize = 0;
  let filename = '';
  const chunks: Buffer[] = [];

  call.on('data', (request: any) => {
    if (!filename) {
      filename = request.filename;
    }

    chunks.push(Buffer.from(request.chunk.data));
    totalSize += request.chunk.data.length;

    // Backpressure: pause if buffer gets too large
    if (totalSize > 100 * 1024 * 1024) {
      call.pause();
    }
  });

  call.on('end', () => {
    const fileBuffer = Buffer.concat(chunks);
    const filePath = `./uploads/${filename}`;

    fs.writeFileSync(filePath, fileBuffer);

    callback(null, {
      filename,
      size: totalSize,
      success: true
    });
  });

  call.on('error', (error: any) => {
    callback(error);
  });
}

Client sends chunks:

const call = client.uploadFile((error: any, response: any) => {
  if (error) {
    console.error('Upload failed:', error);
  } else {
    console.log(`Uploaded ${response.filename}: ${response.size} bytes`);
  }
});

const fileBuffer = fs.readFileSync('./large-file.bin');
const chunkSize = 64 * 1024; // 64 KB chunks

for (let i = 0; i < fileBuffer.length; i += chunkSize) {
  const chunk = fileBuffer.slice(i, i + chunkSize);
  call.write({
    filename: 'large-file.bin',
    chunk: { data: chunk, sequence: Math.floor(i / chunkSize) }
  });
}

call.end();

Client-streaming is ideal for bulk uploads, analytics ingestion, and metric collection.

Bidirectional Streaming for Chat and Collaborative Features

Both client and server send messages simultaneously:

function chat(call: any) {
  const roomId = generateRoomId();
  const rooms = globalRooms;

  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }

  const room = rooms.get(roomId)!;
  room.add(call);

  call.on('data', (message: any) => {
    // Broadcast to all clients in room
    for (const client of room) {
      if (client !== call) {
        client.write({
          sender: message.sender,
          text: message.text,
          timestamp: Date.now()
        });
      }
    }
  });

  call.on('end', () => {
    room.delete(call);
    if (room.size === 0) {
      rooms.delete(roomId);
    }
  });

  call.on('error', (error: any) => {
    console.error('Chat error:', error);
    room.delete(call);
  });
}

Client sends and receives messages:

const call = client.chat();

call.on('data', (message: any) => {
  console.log(`${message.sender}: ${message.text}`);
});

call.on('end', () => {
  console.log('Chat ended');
});

// Send messages
setInterval(() => {
  call.write({
    sender: 'user-1',
    text: 'Hello',
    timestamp: Date.now()
  });
}, 1000);

// Close on disconnect
process.on('SIGINT', () => {
  call.end();
  process.exit(0);
});

Bidirectional streaming powers real-time collaboration, multiplayer games, and live chat.

Flow Control and Backpressure

Clients can overwhelm servers. Implement backpressure:

function uploadFile(call: any, callback: any) {
  let bufferedSize = 0;
  const maxBuffer = 10 * 1024 * 1024; // 10 MB

  call.on('data', (request: any) => {
    bufferedSize += request.chunk.data.length;

    if (bufferedSize > maxBuffer) {
      call.pause();
      console.log('Paused: buffer full');

      // Resume after processing
      setTimeout(() => {
        call.resume();
        bufferedSize = 0;
      }, 1000);
    }
  });
}

On the client:

const call = client.uploadFile(callback);

call.on('drain', () => {
  console.log('Resumed sending');
  // Continue sending chunks
  for (let i = 0; i < 100; i++) {
    const canContinue = call.write(chunk);
    if (!canContinue) {
      console.log('Paused: waiting for drain');
      break;
    }
  }
});

const canContinue = call.write(firstChunk);
if (!canContinue) {
  console.log('Initial pause');
}

Backpressure prevents buffer overruns and OOM crashes.

gRPC Streaming for AI Token Streaming

Stream LLM tokens:

service AIService {
  rpc GenerateText(TextRequest) returns (stream TextToken);
}

message TextRequest {
  string prompt = 1;
}

message TextToken {
  string token = 1;
  int32 index = 2;
}

Server:

async function generateText(call: any) {
  const prompt = call.request.prompt;

  for (let i = 0; i < 100; i++) {
    const token = await llm.getToken(prompt, i);
    call.write({
      token,
      index: i
    });

    // Small delay to simulate streaming
    await new Promise((r) => setTimeout(r, 10));
  }

  call.end();
}

Client accumulates tokens:

const call = client.generateText({ prompt: 'What is AI?' });

let fullText = '';

call.on('data', (response: any) => {
  fullText += response.token;
  console.log(`Token ${response.index}: ${response.token}`);
  updateUI(fullText);
});

call.on('end', () => {
  console.log('Generation complete');
});

gRPC streaming delivers tokens with minimal latency, perfect for AI inference.

gRPC-Web for Browser Clients

gRPC doesn't work directly in browsers (HTTP/2). Use gRPC-Web:

npm install @grpc/grpc-web

Proxy setup (Envoy):

admin:
  address:
    socket_address: { address: '0.0.0.0', port_value: 9901 }

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: '0.0.0.0', port_value: 8080 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                access_logs:
                  - name: envoy.access_loggers.stdout
                http_filters:
                  - name: envoy.filters.http.grpc_web
                    typed_config:
                      '@type': type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
                  - name: envoy.filters.http.router
                    typed_config:
                      '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains: ['*']
                      routes:
                        - match: { prefix: '/' }
                          route:
                            cluster: backend
                            timeout: 0s

clusters:
  - name: backend
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: backend
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 'localhost'
                    port_value: 50051

Browser client:

import { StockServiceClient } from './generated/stock_grpc_web_pb';
import { StockRequest } from './generated/stock_pb';

const client = new StockServiceClient('http://localhost:8080');

const request = new StockRequest();
request.setSymbol('AAPL');

const stream = client.streamPrices(request, {});

stream.on('data', (response: any) => {
  console.log(`${response.getSymbol()}: $${response.getPrice()}`);
});

stream.on('end', () => {
  console.log('Stream ended');
});

gRPC-Web brings gRPC to browsers, enabling real-time, low-latency web apps.

gRPC vs WebSockets for Streaming

FeaturegRPCWebSocket
BinaryYesOptional
MultiplexingYes (HTTP/2)No
Server-pushYesYes
BidirectionalYesYes
Browser supportVia gRPC-WebNative
ComplexityHigherLower

Use gRPC for microservices, backends, and APIs where performance matters. Use WebSockets for browser-based real-time when simplicity is valued.

Checklist

  • Define streaming RPCs in protobuf
  • Generate code from protobuf
  • Implement server-streaming for live data
  • Implement client-streaming for bulk uploads
  • Implement bidirectional streaming
  • Add flow control and backpressure
  • Set up gRPC-Web for browser clients
  • Configure Envoy proxy
  • Test reconnection and error handling
  • Monitor streaming connections and latency

Conclusion

gRPC streaming runs on HTTP/2, providing true multiplexing and low-level control. Three streaming types handle different patterns: server-streaming for real-time data, client-streaming for uploads, bidirectional for collaboration.

For internal APIs and microservices, gRPC is superior to REST. For browsers, use gRPC-Web. The binary protocol and multiplexing reduce latency and bandwidth compared to JSON over HTTP/1.1.

Start with server-streaming for dashboards. Add client-streaming for uploads. Graduate to bidirectional for complex interactions. gRPC's power lies in simplicity at scale.