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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An atomic decrement lets you check and consume quota in a single race-free operation.
- 2Setting the counter's expiry only on its first decrement gives you a naturally resetting daily window.
- 3Bounding the Redis call with a context timeout keeps a slow dependency from stalling every request.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/per-tenant-daily-rate-limiting-in-gin-explained-go-b70e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.