gRPC Cheatsheet
Interceptors
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 are Interceptors
gRPC interceptors wrap RPC calls to inject cross-cutting concerns (auth, logging, tracing, retries, metrics) without touching handler logic. There are two kinds:
| Kind | Applies to | Intercepts |
|---|---|---|
| Client interceptor | Client stub | Outgoing calls (before request sent, after response received) |
| Server interceptor | grpc.Server | Incoming calls (before handler runs, after handler responds) |
Client Interceptors
Interceptor function signature
// An interceptor is a function that receives (options, nextCall) and returns a new InterceptingCall function myInterceptor(options, nextCall) { // options.method_definition — contains path, request/response serializers, etc. return new grpc.InterceptingCall(nextCall(options), { // Outbound hooks (called when the CLIENT does something) start(metadata, listener, next) { // Intercept start of call — good place to add auth headers metadata.add('x-intercepted', 'true'); next(metadata, { // Inbound hooks (called when SERVER responds) onReceiveMetadata(metadata, next) { // Intercept server's initial metadata next(metadata); }, onReceiveMessage(message, next) { // Intercept each response message next(message); }, onReceiveStatus(status, next) { // Intercept final status — good for logging / error mapping next(status); }, }); }, sendMessage(message, next) { // Intercept outgoing request message next(message); }, halfClose(next) { // Intercept end of client stream next(); }, cancel(next) { // Intercept cancellation next(); }, }); }
Attaching interceptors to a call
// Per-call const meta = new grpc.Metadata(); client.getUser({ user_id: 1 }, meta, { interceptors: [myInterceptor] }, callback); // Per-stub (all calls on this client) const client = new proto.UserService('localhost:50051', creds, { interceptors: [loggingInterceptor, authInterceptor], }); // Interceptors run left-to-right (loggingInterceptor wraps authInterceptor)
Common Client Interceptor Patterns
Auth token injector
function authInterceptor(options, nextCall) { return new grpc.InterceptingCall(nextCall(options), { start(metadata, listener, next) { metadata.set('authorization', `Bearer ${getAccessToken()}`); next(metadata, listener); }, }); }
Request/response logger
function loggingInterceptor(options, nextCall) { const method = options.method_definition.path; let request; return new grpc.InterceptingCall(nextCall(options), { start(metadata, listener, next) { next(metadata, { onReceiveMessage(message, next) { console.log(`[${method}] response:`, JSON.stringify(message)); next(message); }, onReceiveStatus(status, next) { console.log(`[${method}] status:`, status.code); next(status); }, onReceiveMetadata: (meta, next) => next(meta), }); }, sendMessage(message, next) { console.log(`[${method}] request:`, JSON.stringify(message)); next(message); }, }); }
Deadline enforcer
function deadlineInterceptor(defaultMs) { return (options, nextCall) => { if (!options.deadline) { options = { ...options, deadline: new Date(Date.now() + defaultMs) }; } return new grpc.InterceptingCall(nextCall(options), {}); }; } const client = new proto.UserService('localhost:50051', creds, { interceptors: [deadlineInterceptor(5_000)], // 5s default on all calls });
Retry on UNAVAILABLE
Do not hand-roll retries inside a client interceptor — grpc.InterceptingCall has no channel access and no supported way to re-issue the underlying call. Use the built-in retry support (retryPolicy in the service config) instead:
const client = new proto.UserService('localhost:50051', creds, { 'grpc.service_config': JSON.stringify({ methodConfig: [{ name: [{}], // all methods; or [{ service: 'user.UserService' }] retryPolicy: { maxAttempts: 4, initialBackoff: '0.1s', maxBackoff: '1s', backoffMultiplier: 2, retryableStatusCodes: ['UNAVAILABLE'], }, }], }), });
For retries that need application logic (jitter, retrying RESOURCE_EXHAUSTED, idempotency checks), wrap the promisified call in a plain retry loop — see callWithRetry in the error-handling topic.
Metrics / tracing
function metricsInterceptor(options, nextCall) { const start = Date.now(); const method = options.method_definition.path; return new grpc.InterceptingCall(nextCall(options), { start(metadata, listener, next) { next(metadata, { onReceiveStatus(status, next) { const latencyMs = Date.now() - start; metrics.histogram('grpc.client.latency', latencyMs, { method, code: status.code }); next(status); }, onReceiveMessage: (msg, next) => next(msg), onReceiveMetadata: (meta, next) => next(meta), }); }, }); }
Server Interceptors
Server interceptors require @grpc/grpc-js >= 1.9. An interceptor receives (methodDescriptor, call) and returns a ServerInterceptingCall wrapping a responder (outbound hooks). Inbound events — including the request metadata — arrive through a server listener that you pass to the responder's start(next) via next(listener).
const { ServerInterceptingCall, ServerListenerBuilder } = grpc; function serverLoggingInterceptor(methodDescriptor, call) { const method = methodDescriptor.path; const start = Date.now(); return new ServerInterceptingCall(call, { start(next) { // Fires when the call starts; call next() (optionally with a listener) console.log(`[${method}] call started`); next(); }, sendMessage(message, next) { // Intercept each outgoing server message next(message); }, sendStatus(status, next) { // Fires when the call completes console.log(`[${method}] done in ${Date.now() - start}ms, code=${status.code}`); next(status); }, }); } function serverAuthInterceptor(methodDescriptor, call) { // Request metadata is delivered to the LISTENER's onReceiveMetadata hook — // it is NOT available as a property on `call`. const listener = new ServerListenerBuilder() .withOnReceiveMetadata((metadata, next) => { const token = metadata.get('authorization')[0]?.replace('Bearer ', ''); if (!token || !verifyToken(token)) { call.sendStatus({ code: grpc.status.UNAUTHENTICATED, details: 'invalid token', }); return; // do NOT call next() — the handler never runs } next(metadata); }) .build(); return new ServerInterceptingCall(call, { start: (next) => next(listener), }); } // Attach to server const server = new grpc.Server({ interceptors: [serverAuthInterceptor, serverLoggingInterceptor], // Interceptors run left-to-right: auth first, then logging });
Interceptor Execution Order
Client call → [interceptorA.start] → [interceptorB.start] → network → server
← [interceptorB.onReceiveMessage] ← [interceptorA.onReceiveMessage] ← server responseInterceptors are stacked: the first interceptor in the array is the outermost wrapper. For client interceptors, outbound hooks run left-to-right; inbound hooks run right-to-left.
Interceptor Factories
// Parameterize interceptors with a factory function function createAuthInterceptor(tokenProvider) { return function authInterceptor(options, nextCall) { return new grpc.InterceptingCall(nextCall(options), { start(metadata, listener, next) { tokenProvider().then((token) => { metadata.set('authorization', `Bearer ${token}`); next(metadata, listener); }).catch((err) => { // Terminate the call with an auth error listener.onReceiveStatus({ code: grpc.status.UNAUTHENTICATED, details: err.message, metadata: new grpc.Metadata(), }); }); }, }); }; } const client = new proto.UserService('localhost:50051', creds, { interceptors: [createAuthInterceptor(fetchOAuthToken)], });
Interceptor vs callCreds
| Approach | Best for |
|---|---|
credentials.createFromMetadataGenerator | Simple token injection at channel level |
| Client interceptor | Complex per-call logic, logging, retries, transformations |
| Server interceptor | Auth, logging, tracing, rate limiting on the server side |
Gotchas
Not calling
next()— if you omitnext()in any hook, the call hangs. Always callnext()or explicitly send a status to terminate the call.
Async in
start— if you need toawaitsomething (e.g., fetch a token), ensure you handle promise rejection and callnext()or terminate the call. Uncaught rejections will leave the call hanging.
Order matters — auth interceptor should be outermost (first in array) so it can reject calls before logging or tracing interceptors see them. Or innermost if you want logging to capture rejected calls too — it depends on your observability needs.
Server interceptors in grpc-js — the
ServerInterceptingCallAPI stabilized in@grpc/grpc-js1.9. Older versions have no server interceptor support.