gRPC Cheatsheet

Unary Calls

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

What is a Unary Call

A unary RPC is the simplest pattern: the client sends one request and the server returns one response, just like an HTTP request/response.

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

Server Handler

// handlers/user.js
function getUser(call, callback) {
  const { user_id } = call.request;

  db.findById(user_id, (err, user) => {
    if (err)   return callback({ code: grpc.status.INTERNAL, message: err.message });
    if (!user) return callback({ code: grpc.status.NOT_FOUND, message: `user ${user_id} not found` });
    callback(null, { user });
  });
}

// Async variant
async function getUserAsync(call, callback) {
  try {
    const user = await db.findById(call.request.user_id);
    if (!user) return callback({ code: grpc.status.NOT_FOUND, message: 'not found' });
    callback(null, { user });
  } catch (err) {
    callback({ code: grpc.status.INTERNAL, message: err.message });
  }
}

Client — Callback Style

const meta = new grpc.Metadata();
meta.add('x-request-id', 'req-001');

const deadline = new Date(Date.now() + 3000);   // 3 s

client.getUser(
  { user_id: 42 },           // request
  meta,                      // optional metadata (can omit)
  { deadline },              // optional options (can omit)
  (err, response) => {
    if (err) {
      console.error(err.code, err.message);
      return;
    }
    console.log(response.user);
  },
);

Argument Overloads

// All of these are valid:
client.getUser(request, callback)
client.getUser(request, metadata, callback)
client.getUser(request, options, callback)
client.getUser(request, metadata, options, callback)

Client — Promise Style

// util.promisify
const { promisify } = require('util');
const getUser = promisify(client.getUser.bind(client));

try {
  const response = await getUser({ user_id: 42 });
  console.log(response.user);
} catch (err) {
  console.error(err.code, err.message);
}
// Manual wrapper (includes metadata + deadline)
function rpc(method, request, { metadata, deadlineMs } = {}) {
  return new Promise((resolve, reject) => {
    const meta = metadata ?? new grpc.Metadata();
    const opts = deadlineMs ? { deadline: new Date(Date.now() + deadlineMs) } : {};
    method.call(client, request, meta, opts, (err, res) => {
      if (err) reject(err);
      else resolve(res);
    });
  });
}

const response = await rpc(client.getUser, { user_id: 42 }, { deadlineMs: 3000 });

Cancellation

const call = client.getUser({ user_id: 42 }, (err, res) => {
  if (err?.code === grpc.status.CANCELLED) {
    return console.log('call was cancelled');
  }
  console.log(res.user);
});

// Cancel after 1 second
const timer = setTimeout(() => call.cancel(), 1000);

// Clear the timer if call completes normally
// (the callback handles both paths)

Reading Header and Trailer Metadata

const call = client.getUser({ user_id: 42 }, (err, res) => {
  if (err) return console.error(err);
  console.log(res.user);
});

call.on('metadata', (headers) => {
  console.log('initial headers:', headers.getMap());
});

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

Server — Sending Trailing Metadata

function getUser(call, callback) {
  const user = db.findById(call.request.user_id);

  const trailer = new grpc.Metadata();
  trailer.set('x-served-by', process.env.HOSTNAME);

  // 3rd arg to callback = trailing metadata
  callback(null, { user }, trailer);
}

Server — Sending Initial Metadata Early

// For unary calls sendMetadata must be called BEFORE callback
function getUser(call, callback) {
  const header = new grpc.Metadata();
  header.set('x-request-id', generateId());
  call.sendMetadata(header);    // sends immediately

  const user = db.findById(call.request.user_id);
  callback(null, { user });
}

Error Object Reference

// Shape of the error passed to callback (on error path)
{
  code:     grpc.status.NOT_FOUND,   // numeric status code
  message:  'user not found',        // short human-readable string
  details:  'no row in users table', // optional extended detail
  metadata: new grpc.Metadata(),     // optional trailing metadata
}
StatusWhen to use
NOT_FOUNDEntity does not exist
INVALID_ARGUMENTClient sent bad input
ALREADY_EXISTSCreate conflicts with existing record
PERMISSION_DENIEDAuthenticated but not authorized
UNAUTHENTICATEDMissing or invalid credentials
RESOURCE_EXHAUSTEDRate limit hit
INTERNALUnexpected server-side error
UNAVAILABLEServer not ready — safe to retry
DEADLINE_EXCEEDEDOperation timed out

Full Round-Trip Example

// user.proto
syntax = "proto3";
package myapp.v1;

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  int32 user_id = 1;
}
message GetUserResponse {
  User user = 1;
}
message User {
  int32  id    = 1;
  string name  = 2;
  string email = 3;
}
// server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const def = protoLoader.loadSync('./user.proto', { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true });
const { myapp: { v1: proto } } = grpc.loadPackageDefinition(def);

const db = { 1: { id: 1, name: 'Alice', email: 'alice@example.com' } };

function getUser({ request }, callback) {
  const user = db[request.user_id];
  if (!user) return callback({ code: grpc.status.NOT_FOUND, message: 'not found' });
  callback(null, { user });
}

const server = new grpc.Server();
server.addService(proto.UserService.service, { getUser });
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  console.log('listening on 50051');
});
// client.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { promisify } = require('util');

const def = protoLoader.loadSync('./user.proto', { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true });
const { myapp: { v1: proto } } = grpc.loadPackageDefinition(def);

const client = new proto.UserService('localhost:50051', grpc.credentials.createInsecure());
const getUser = promisify(client.getUser.bind(client));

(async () => {
  try {
    const { user } = await getUser({ user_id: 1 });
    console.log(user);  // { id: 1, name: 'Alice', email: 'alice@example.com' }
  } catch (err) {
    console.error(err.code, err.message);
  } finally {
    client.close();
  }
})();

Gotchas

Calling callback twice — calling the callback more than once in a unary handler causes a crash. Guard every early-return path carefully.

async handlers without try/catch — an uncaught rejection will not invoke the callback; the client will hang until its deadline. Always wrap async server handlers.

Metadata before first call — if the client has no waitForReady guard and the server is not yet up, the first call gets UNAVAILABLE immediately (no retry by default unless a retry policy is configured).