π Description
The Core Service has no rate limiting. Every protected endpoint (recommendations, repo search, setup guide) is guarded by JWTMiddleware, which correctly enforces authentication β but any authenticated user can send unlimited requests, which is a real problem because:
- Each request may fan out to the GitHub API and/or the AI/LLM service
- A misbehaving client or a developer running a test loop can trigger GitHub's secondary rate limits and get the service's shared token blocked
- There's no fairness guarantee β one user can starve everyone else
The fix is to add a per-user token bucket middleware. Each user gets a bucket of N tokens that refills at rate R per second. Each request costs 1 token. If the bucket is empty, the request is rejected with HTTP 429 Too Many Requests.
This is a real, production-grade Go task that requires understanding of concurrency, goroutines, sync.Mutex, and HTTP middleware chaining.
π Files to Create / Modify
- Create:
backend/core_service/internal/ratelimit/limiter.go
- Modify:
backend/core_service/routes/routes.go β wrap protected routes with the new middleware
π How Token Bucket Works
Each user has a "bucket" with capacity N tokens.
Tokens refill at rate R per second.
Each request consumes 1 token.
If the bucket is empty β reject the request β HTTP 429.
Example: capacity=10, refillRate=2 per second means a user can burst 10 requests then is limited to 2/s.
β
What To Build
Step 1 β Create backend/core_service/internal/ratelimit/limiter.go:
package ratelimit
import (
"net/http"
"sync"
"time"
)
type bucket struct {
tokens float64
capacity float64
refillRate float64 // tokens per second
lastRefill time.Time
mu sync.Mutex
}
func (b *bucket) allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
elapsed := now.Sub(b.lastRefill).Seconds()
b.lastRefill = now
// Refill tokens based on elapsed time
b.tokens += elapsed * b.refillRate
if b.tokens > b.capacity {
b.tokens = b.capacity
}
if b.tokens >= 1.0 {
b.tokens--
return true
}
return false
}
// Limiter holds per-user buckets
type Limiter struct {
mu sync.RWMutex
buckets map[string]*bucket
capacity float64
rate float64
}
func NewLimiter(capacity float64, ratePerSecond float64) *Limiter {
return &Limiter{
buckets: make(map[string]*bucket),
capacity: capacity,
rate: ratePerSecond,
}
}
func (l *Limiter) getBucket(userID string) *bucket {
// Fast path: read lock
l.mu.RLock()
b, ok := l.buckets[userID]
l.mu.RUnlock()
if ok {
return b
}
// Slow path: write lock to create new bucket
l.mu.Lock()
defer l.mu.Unlock()
// Double-check after acquiring write lock
if b, ok = l.buckets[userID]; ok {
return b
}
b = &bucket{
tokens: l.capacity,
capacity: l.capacity,
refillRate: l.rate,
lastRefill: time.Now(),
}
l.buckets[userID] = b
return b
}
// Middleware wraps an http.Handler with per-user rate limiting.
// It reads the authenticated user ID from the request context
// (set by JWTMiddleware using the "user_id" key).
func (l *Limiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value(contextKey("user_id")).(string)
if !ok || userID == "" {
// Should not happen if JWTMiddleware ran first, but be safe
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
b := l.getBucket(userID)
if !b.allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, `{"error":"rate limit exceeded, please slow down"}`, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
// Must match the key type in auth/middleware.go
type contextKey string
Step 2 β Wire it into routes/routes.go:
// In the route setup function, after creating JWTMiddleware:
limiter := ratelimit.NewLimiter(10, 2) // 10 burst, 2 req/sec sustained
// Example: wrap the recommendations handler
mux.Handle("/recommendations",
auth.JWTMiddleware(jwtSecret,
limiter.Middleware(
http.HandlerFunc(recHandler.GetRecommendation),
),
),
)
The middleware chain is: JWTMiddleware β RateLimitMiddleware β Handler. This order matters β rate limiting only makes sense for authenticated users (known by user ID).
Step 3 (Bonus) β Add periodic cleanup to avoid unbounded memory growth:
If the service runs for weeks, the buckets map grows forever (one entry per unique user). Add a cleanup goroutine that removes buckets for users who haven't made a request in a configurable duration (e.g., 1 hour).
π Acceptance Criteria
π‘ Technical Hints
- Look at
backend/core_service/internal/auth/middleware.go for how user_id is stored in context using context.WithValue. Your middleware reads that same key. Be careful: you must use the same contextKey type, otherwise r.Context().Value("user_id") will return nil because Go context keys are compared by both value AND type.
- The double-checked locking pattern in
getBucket (check RLock β check WLock) is important for concurrent correctness and performance. Without it, acquiring a write lock on every request would be a bottleneck.
- Test your rate limiter by writing a
TestRateLimiter in limiter_test.go that fires 15 requests rapidly and asserts that the first 10 succeed and the next 5 are rejected.
π Getting Started
- Fork the repository
- Create a branch:
git checkout -b feat/issue-29-rate-limiting
- Create
backend/core_service/internal/ratelimit/limiter.go
- Write tests in
backend/core_service/internal/ratelimit/limiter_test.go
- Wire into
routes/routes.go
- Run:
cd backend/core_service && go test -race ./...
- Open a PR with test output showing the rate limiter working!
π Description
The Core Service has no rate limiting. Every protected endpoint (recommendations, repo search, setup guide) is guarded by
JWTMiddleware, which correctly enforces authentication β but any authenticated user can send unlimited requests, which is a real problem because:The fix is to add a per-user token bucket middleware. Each user gets a bucket of N tokens that refills at rate R per second. Each request costs 1 token. If the bucket is empty, the request is rejected with
HTTP 429 Too Many Requests.This is a real, production-grade Go task that requires understanding of concurrency, goroutines,
sync.Mutex, and HTTP middleware chaining.π Files to Create / Modify
backend/core_service/internal/ratelimit/limiter.gobackend/core_service/routes/routes.goβ wrap protected routes with the new middlewareπ How Token Bucket Works
Example:
capacity=10, refillRate=2 per secondmeans a user can burst 10 requests then is limited to 2/s.β What To Build
Step 1 β Create
backend/core_service/internal/ratelimit/limiter.go:Step 2 β Wire it into
routes/routes.go:The middleware chain is:
JWTMiddleware β RateLimitMiddleware β Handler. This order matters β rate limiting only makes sense for authenticated users (known by user ID).Step 3 (Bonus) β Add periodic cleanup to avoid unbounded memory growth:
If the service runs for weeks, the
bucketsmap grows forever (one entry per unique user). Add a cleanup goroutine that removes buckets for users who haven't made a request in a configurable duration (e.g., 1 hour).π Acceptance Criteria
Limiterstruct exists inratelimit/limiter.gousing the token bucket algorithmbucket.allow()method is correct: refills on elapsed time, caps at capacity, returnsfalsewhen emptyRetry-After: 1header when the limit is exceededuser_idfrom the request context (set byJWTMiddleware)/recommendationsroute is wrapped with the rate limitergo test -race ./internal/ratelimit/...passes with zero data racesπ‘ Technical Hints
backend/core_service/internal/auth/middleware.gofor howuser_idis stored in context usingcontext.WithValue. Your middleware reads that same key. Be careful: you must use the samecontextKeytype, otherwiser.Context().Value("user_id")will returnnilbecause Go context keys are compared by both value AND type.getBucket(check RLock β check WLock) is important for concurrent correctness and performance. Without it, acquiring a write lock on every request would be a bottleneck.TestRateLimiterinlimiter_test.gothat fires 15 requests rapidly and asserts that the first 10 succeed and the next 5 are rejected.π Getting Started
git checkout -b feat/issue-29-rate-limitingbackend/core_service/internal/ratelimit/limiter.gobackend/core_service/internal/ratelimit/limiter_test.goroutes/routes.gocd backend/core_service && go test -race ./...