Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
14 Jul 2025 · 4 min read ·Article 36 / 110
Go

36 Rate Limiting in Interceptors

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

In modern microservice development, we’re not only concerned with performance and scalability, but also with how to protect a service from abuse and excessive traffic spikes. One important mechanism for this is rate limiting.

This article discusses how to apply rate limiting elegantly at the interceptor level in gRPC using Go. We’ll cover the concepts, the implementation, and the best practices so that your service stays resilient under heavy load.


What Is Rate Limiting?

Rate limiting is a technique for restricting the number of requests a client can make within a given period of time. For example:

  • 10 requests per second per user
  • 1000 requests per minute per IP address

The main goals are:

  • 🚫 Preventing abuse and spam
  • 🧱 Protecting the service from overloading
  • 🧭 Maintaining fairness between clients

Approaches to Rate Limiting in gRPC

gRPC provides interceptor support on the server side (and the client side). We can insert a rate limiter into a UnaryServerInterceptor so that every request is first checked to see whether it is still within the allowed limit.

For the implementation in Go, we can use:

  • golang.org/x/time/rate (leaky bucket)
  • Open-source libraries such as uber-go/ratelimit

Example Scenario: 5 Requests per Second per Client

Installing the Dependency

bash
1go get golang.org/x/time/rate

Implementing the Rate Limiter Interceptor

go
 1package middleware
 2
 3import (
 4    "context"
 5    "sync"
 6    "time"
 7
 8    "google.golang.org/grpc"
 9    "google.golang.org/grpc/metadata"
10    "google.golang.org/grpc/status"
11    "google.golang.org/grpc/codes"
12
13    "golang.org/x/time/rate"
14)
15
16// rateLimiter stores a limiter per client (e.g. per API key or IP)
17var rateLimiter = struct {
18    sync.RWMutex
19    clients map[string]*rate.Limiter
20}{clients: make(map[string]*rate.Limiter)}
21
22func getClientLimiter(clientID string) *rate.Limiter {
23    rateLimiter.RLock()
24    limiter, exists := rateLimiter.clients[clientID]
25    rateLimiter.RUnlock()
26
27    if !exists {
28        limiter = rate.NewLimiter(5, 10) // 5 req/s, max burst 10
29        rateLimiter.Lock()
30        rateLimiter.clients[clientID] = limiter
31        rateLimiter.Unlock()
32    }
33    return limiter
34}
35
36func RateLimitInterceptor() grpc.UnaryServerInterceptor {
37    return func(
38        ctx context.Context,
39        req interface{},
40        info *grpc.UnaryServerInfo,
41        handler grpc.UnaryHandler,
42    ) (interface{}, error) {
43        md, ok := metadata.FromIncomingContext(ctx)
44        if !ok {
45            return nil, status.Errorf(codes.Unauthenticated, "metadata not found")
46        }
47
48        clientID := "anonymous"
49        if vals := md.Get("x-api-key"); len(vals) > 0 {
50            clientID = vals[0]
51        }
52
53        limiter := getClientLimiter(clientID)
54
55        if !limiter.Allow() {
56            return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded")
57        }
58
59        return handler(ctx, req)
60    }
61}

Integrating with the gRPC Server

go
1s := grpc.NewServer(
2    grpc.UnaryInterceptor(RateLimitInterceptor()),
3)
4pb.RegisterMyServiceServer(s, &MyService{})

Client Simulation Table

Client IDRequest LimitMax BurstInitial StatusStatus After 10 Requests
client-a5 req/s10✅ Allowed❌ Blocked (Rate Limit)
client-b5 req/s10✅ Allowed✅ Allowed (due to 200ms gaps)

Time vs. Request Status Simulation Chart

MERMAID
gantt
title Simulasi Permintaan Client A (Limit 5 req/s, burst 10)
dateFormat  X
section Status
Request ke-1    :done, 1, 1s
Request ke-2    :done, 2, 1s
Request ke-3    :done, 3, 1s
Request ke-4    :done, 4, 1s
Request ke-5    :done, 5, 1s
Request ke-6    :done, 6, 1s
Request ke-7    :done, 7, 1s
Request ke-8    :done, 8, 1s
Request ke-9    :done, 9, 1s
Request ke-10   :done, 10, 1s
Request ke-11   :crit, 11, 1s
Request ke-12   :crit, 12, 1s

Best Practices

Use metadata or JWT claims as the key: an API key, user ID, or IP address. ✅ Choose the burst size wisely: keep it limited so spikes don’t get through. ✅ Reset the limiter map periodically: avoid memory leaks from a map that keeps growing. ✅ Logging & metrics: add logs for requests rejected due to the limit, and expose metrics (Prometheus).


Bonus: Visualizing the Request Flow

MERMAID
flowchart TD
  Client -->|Request + Metadata| Interceptor
  Interceptor -->|"Check Allow()"| Handler
  Interceptor -->|Reject if Exceeded| ErrorResponse

Case Study: Limits Based on Role

If you have a JWT or metadata that contains the user’s role, you can create different rate limits:

go
1if role == "free" {
2    limiter = rate.NewLimiter(1, 3)
3} else if role == "premium" {
4    limiter = rate.NewLimiter(10, 20)
5}

With this, you can configure tiered access according to the user’s role or subscription.


Conclusion

Rate limiting is an essential layer in building APIs that are secure, fair, and stable. By placing it in a gRPC interceptor, you ensure that traffic validation happens at the system’s entry point.

Use this approach for:

  • 🔐 Public APIs
  • 🧩 Internal microservices between teams
  • 🧰 A gRPC gateway acting as an aggregator

Related Articles

💬 Comments