- Published on
gRPC Streaming in 2026 — Server Streaming, Client Streaming, and Bidirectional
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
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
- Defining Streaming RPCs in Protobuf
- Node.js @grpc/grpc-js Streaming Implementation
- Server-Streaming for Real-Time Data (Stock Prices, Logs)
- Client-Streaming for File Upload
- Bidirectional Streaming for Chat and Collaborative Features
- Flow Control and Backpressure
- gRPC Streaming for AI Token Streaming
- gRPC-Web for Browser Clients
- gRPC vs WebSockets for Streaming
- Checklist
- Conclusion
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
| Feature | gRPC | WebSocket |
|---|---|---|
| Binary | Yes | Optional |
| Multiplexing | Yes (HTTP/2) | No |
| Server-push | Yes | Yes |
| Bidirectional | Yes | Yes |
| Browser support | Via gRPC-Web | Native |
| Complexity | Higher | Lower |
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.