36 Rate Limiting in Interceptors
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
1go get golang.org/x/time/rateImplementing the Rate Limiter Interceptor
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
1s := grpc.NewServer(
2 grpc.UnaryInterceptor(RateLimitInterceptor()),
3)
4pb.RegisterMyServiceServer(s, &MyService{})Client Simulation Table
| Client ID | Request Limit | Max Burst | Initial Status | Status After 10 Requests |
|---|---|---|---|---|
| client-a | 5 req/s | 10 | ✅ Allowed | ❌ Blocked (Rate Limit) |
| client-b | 5 req/s | 10 | ✅ Allowed | ✅ Allowed (due to 200ms gaps) |
Time vs. Request Status Simulation Chart
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
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:
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