gRPC Cheatsheet

Streaming

Use this gRPC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Streaming Types Overview

PatternProto syntaxServer writesClient writesUse case
Server streamingrpc Foo (Req) returns (stream Res)N1Subscriptions, large result sets, live feeds
Client streamingrpc Foo (stream Req) returns (Res)1NFile upload, bulk ingest, sensor batching
Bidirectionalrpc Foo (stream Req) returns (stream Res)NNChat, real-time sync, collaborative editing

Proto Definitions

service StreamService {
  rpc ListItems   (ListItemsRequest)    returns (stream Item);           // server streaming
  rpc UploadItems (stream UploadChunk)  returns (UploadSummary);        // client streaming
  rpc Sync        (stream SyncRequest)  returns (stream SyncResponse);  // bidi streaming
}

Server Streaming

Server handler

function listItems(call) {
  // call.request  — the single incoming request message
  // call.write()  — send one item to the client
  // call.end()    — signal end of stream (required)
  // call.cancelled — boolean; check before expensive ops
  // call.on('cancelled', cb) — fires if client disconnects

  call.on('cancelled', () => {
    console.log('client cancelled');
    // clean up database cursor, etc.
  });

  for (const item of db.getAll()) {
    if (call.cancelled) return;
    call.write({ item });
  }
  call.end();
}

// Async generator pattern
async function listItemsAsync(call) {
  for await (const item of db.streamAll()) {
    if (call.cancelled) return;
    call.write({ item });
  }
  call.end();
}

Client consuming server stream

const stream = client.listItems({ filter: 'active' });

stream.on('data', (response) => {
  console.log('received:', response.item);
});

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

stream.on('error', (err) => {
  if (err.code === grpc.status.CANCELLED) return;   // we cancelled it
  console.error('stream error', err.code, err.message);
});

stream.on('status', (status) => {
  console.log('final status:', status.code);
  console.log('trailing metadata:', status.metadata.getMap());
});

// Cancel the stream early
setTimeout(() => stream.cancel(), 2000);

Async iterator (Node 10+)

async function consumeStream() {
  const stream = client.listItems({ filter: 'active' });
  try {
    for await (const response of stream) {
      console.log(response.item);
    }
  } catch (err) {
    if (err.code !== grpc.status.CANCELLED) throw err;
  }
}

Client Streaming

Client sending

const call = client.uploadItems((err, response) => {
  // called once when the server responds
  if (err) return console.error(err);
  console.log('uploaded', response.count, 'items');
});

// Optionally attach metadata listeners
call.on('metadata', (meta) => console.log('headers:', meta.getMap()));

// Write chunks
for (const chunk of chunks) {
  call.write({ data: chunk.data, filename: chunk.name });
}

// Signal end of client stream — server will now respond
call.end();

// Cancel mid-upload
// call.cancel()

Server handler

function uploadItems(call, callback) {
  const results = [];

  call.on('data', (chunk) => {
    // process each client chunk
    results.push(processChunk(chunk));
  });

  call.on('end', () => {
    // client done sending — send the single response
    callback(null, { count: results.length, ids: results });
  });

  call.on('error', (err) => {
    console.error('client stream error:', err);
  });
}

Async/await client streaming

function streamToPromise(call, chunks) {
  return new Promise((resolve, reject) => {
    call.on('error', reject);
    for (const chunk of chunks) call.write(chunk);
    call.end();
    // response comes via callback passed at call creation
  });
}

Bidirectional Streaming

Server handler

function sync(call) {
  // call is a duplex stream: readable (client messages) + writable (server messages)

  call.on('data', (request) => {
    // process incoming message
    const response = processRequest(request);
    call.write(response);   // respond to this message
  });

  call.on('end', () => {
    // client closed its write side
    call.end();             // close server write side
  });

  call.on('error', (err) => {
    console.error('bidi stream error', err);
  });

  call.on('cancelled', () => {
    console.log('client cancelled');
  });
}

Client handler

const call = client.sync();

call.on('data', (response) => {
  console.log('server says:', response);
});

call.on('end', () => {
  console.log('server closed stream');
});

call.on('error', (err) => {
  if (err.code !== grpc.status.CANCELLED) console.error(err);
});

// Send messages
call.write({ action: 'push', payload: '...' });
call.write({ action: 'push', payload: '...' });

// Signal we're done sending
call.end();

Bidi with async iteration

// Node.js Duplex streams are async iterable (for await...of reads)
async function runSync(messages) {
  const call = client.sync();

  // Send in background
  (async () => {
    for (const msg of messages) {
      call.write(msg);
      await sleep(100);
    }
    call.end();
  })();

  // Receive
  for await (const response of call) {
    console.log('received', response);
  }
}

Backpressure

// call.write() returns false when the internal buffer is full
// Resume when 'drain' fires
function writeWithBackpressure(call, items, onDone) {
  let i = 0;

  function writeNext() {
    while (i < items.length) {
      const ok = call.write(items[i++]);
      if (!ok) {
        call.once('drain', writeNext);    // wait for drain
        return;
      }
    }
    call.end();
    onDone();
  }

  writeNext();
}

Flow Control Options

// Disable automatic flow control on readable (server/bidi streams)
// Use call.resume() to pull data manually
const stream = client.listItems({ filter: 'active' });
stream.pause();    // pause immediately

setTimeout(() => {
  stream.resume();    // start consuming after 1s delay
}, 1000);

Sending Metadata on Streaming Calls

// Server: send initial metadata before first write
function listItems(call) {
  const header = new grpc.Metadata();
  header.set('x-total-estimate', '1000');
  call.sendMetadata(header);    // fires before any call.write()

  for (const item of db.getAll()) {
    call.write({ item });
  }

  const trailer = new grpc.Metadata();
  trailer.set('x-items-sent', String(db.count()));
  call.end(trailer);            // trailing metadata in end()
}

// Client: read header and trailer
const stream = client.listItems({});
stream.on('metadata', (header) => console.log('header:', header.getMap()));
stream.on('status', (status) => console.log('trailer:', status.metadata.getMap()));

Common Patterns

Timeout on a streaming call

const deadline = new Date(Date.now() + 30_000);    // 30s budget
const stream = client.listItems({ filter: 'active' }, new grpc.Metadata(), { deadline });

stream.on('error', (err) => {
  if (err.code === grpc.status.DEADLINE_EXCEEDED) {
    console.error('stream timed out');
  }
});

Re-emit errors as stream end

// Pattern: treat errors as stream termination rather than throw
stream.on('error', (err) => {
  if (err.code === grpc.status.OK) return;    // grpc-js emits 'error' even on OK sometimes
  handleError(err);
});

Piping Node.js readable into client stream

const fs = require('fs');
const call = client.uploadItems((err, res) => console.log(res));

const file = fs.createReadStream('./data.bin', { highWaterMark: 64 * 1024 });
file.on('data', (chunk) => {
  call.write({ data: chunk });
});
file.on('end', () => call.end());
file.on('error', (err) => {
  call.cancel();
  console.error(err);
});

Gotchas

Not calling call.end() — on server streaming or bidi handlers, failing to call end() leaves the client hanging indefinitely. Always call it in the 'end' event handler or in a finally block.

Writing after end() — calling call.write() after call.end() throws. Track whether you have called end.

'error' + 'end'grpc-js emits 'error' followed by 'end' on stream failure. Only handle one path to avoid double-processing.

Large message defaults — each written message must be under max_send_message_length (default 4 MB). For large payloads, chunk the data or raise the limit on both sides.

Server-streaming back-pressurecall.write() does not block; if the client is slow the internal buffer grows unboundedly. Use write() return value + 'drain' for flow control on large result sets.