go 54 lines · 8 steps

Per-tenant daily rate limiting in Gin

A Gin middleware that enforces a daily request quota per tenant using an atomic Redis counter.

Explained by highlit
1package middleware
2 
3import (
4 "context"
5 "net/http"
6 "time"
7 
8 "github.com/gin-gonic/gin"
9 "github.com/redis/go-redis/v9"
10)
11 
12func EnforceQuota(rdb *redis.Client, dailyLimit int64) gin.HandlerFunc {
13 return func(c *gin.Context) {
14 tenantID := c.GetString("tenant_id")
15 if tenantID == "" {
16 c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
17 "error": "missing tenant context",
18 })
19 return
20 }
21 
22 ctx, cancel := context.WithTimeout(c.Request.Context(), 200*time.Millisecond)
23 defer cancel()
24 
25 key := "quota:" + tenantID + ":" + time.Now().UTC().Format("2006-01-02")
26 
27 remaining, err := rdb.DecrBy(ctx, key, 1).Result()
28 if err != nil {
29 c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
30 "error": "quota service unavailable",
31 })
32 return
33 }
34 
35 if remaining == dailyLimit-1 {
36 rdb.Expire(ctx, key, 24*time.Hour)
37 }
38 
39 if remaining < 0 {
40 rdb.Incr(ctx, key)
41 c.Header("X-RateLimit-Remaining", "0")
42 c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
43 "error": "daily quota exceeded",
44 "limit": dailyLimit,
45 "retry_after": "24h",
46 })
47 return
48 }
49 
50 c.Header("X-RateLimit-Limit", strconv.FormatInt(dailyLimit, 10))
51 c.Header("X-RateLimit-Remaining", strconv.FormatInt(remaining, 10))
52 c.Next()
53 }
54}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1An atomic decrement lets you check and consume quota in a single race-free operation.
  2. 2Setting the counter's expiry only on its first decrement gives you a naturally resetting daily window.
  3. 3Bounding the Redis call with a context timeout keeps a slow dependency from stalling every request.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Per-tenant daily rate limiting in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code