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:

KindApplies toIntercepts
Client interceptorClient stubOutgoing calls (before request sent, after response received)
Server interceptorgrpc.ServerIncoming 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 response

Interceptors 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

ApproachBest for
credentials.createFromMetadataGeneratorSimple token injection at channel level
Client interceptorComplex per-call logic, logging, retries, transformations
Server interceptorAuth, logging, tracing, rate limiting on the server side

Gotchas

Not calling next() — if you omit next() in any hook, the call hangs. Always call next() or explicitly send a status to terminate the call.

Async in start — if you need to await something (e.g., fetch a token), ensure you handle promise rejection and call next() 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 ServerInterceptingCall API stabilized in @grpc/grpc-js 1.9. Older versions have no server interceptor support.