Skip to content

Interceptors

An interceptor is gRPC’s version of middleware: a function that wraps a call so you can run code before and after the actual handler. If you have used HTTP middleware, this is the same idea, applied to RPCs instead of routes.

The point is to keep cross-cutting logic — logging, authentication, metrics, tracing, retries — out of your business handlers. You write the concern once as an interceptor and it applies to every method automatically.

Interceptors compose into a chain. Each one gets the call, does its work, and calls the next link — until the innermost link is your handler. On the way back out, each interceptor can inspect the response or error.

flowchart LR
  call["incoming call"] --> log["logging
interceptor"]
  log --> auth["auth
interceptor"]
  auth --> metrics["metrics
interceptor"]
  metrics --> handler["your handler"]
  handler -->|response / error| metrics
  metrics --> auth
  auth --> log
  log -->|response| call
An interceptor chain wrapping the handler

The order matters: put logging outermost so it records everything, auth next so unauthenticated calls are rejected before they reach expensive work, and metrics close to the handler so timings reflect real work.

Because gRPC has unary and streaming calls, it has two interceptor types:

  • A unary interceptor wraps a single request/response call. It sees the request, calls the handler, and sees the response — one shot.
  • A stream interceptor wraps a streaming call. It doesn’t see individual messages by default; it wraps the stream object, so to observe each message you wrap the stream’s send/receive methods.

That asymmetry trips people up: a logging interceptor written for unary calls won’t automatically log every streamed message — you have to wrap the stream.

Here is server-side logging that records the method and elapsed time for every unary call.

func loggingUnary(
ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
start := time.Now()
resp, err := handler(ctx, req) // call the next link (eventually your handler)
log.Printf("method=%s took=%s err=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
server := grpc.NewServer(grpc.ChainUnaryInterceptor(loggingUnary))

Interceptors run on both sides: server interceptors enforce auth and record metrics; client interceptors attach tokens, add trace ids, and implement retries with backoff. The same chain idea applies in both directions.

  • Logging & tracing — one place to record method, latency, status, and a trace id.
  • Auth — validate a token from metadata and reject early with UNAUTHENTICATED.
  • Metrics — count calls and observe latency per method for dashboards.
  • Retries — a client interceptor can transparently retry idempotent calls on transient failures.
  • Recovery — catch a panic/exception in a handler and turn it into a clean INTERNAL status instead of crashing the connection.
What is an interceptor in gRPC?
Why put an auth interceptor before the handler in the chain?
Why does a unary logging interceptor not automatically log every streamed message?
Which task is a good fit for a client-side interceptor?