Skip to content

User-Based Rate Limiting — Per-Account Traffic Control for Authenticated APIs

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about User. We cover key concepts, practical examples, and best practices to help you master this topic.

User-based Rate Limiting applies limits to authenticated users identified by their user ID, API key, or JWT claims, enabling tiered limits per subscription level, granular quotas, and per-endpoint restrictions.

What You'll Learn

  • Extracting user identity from authentication tokens
  • Tiered rate limits based on subscription level
  • Per-endpoint user rate limiting

Why It Matters

IP-based limits punish users behind NAT and miss abuse from rotating IPs. User-based limits track the actual account, regardless of IP. Combined with tiered pricing, they enable monetization of API access.

Real-World Use

Durga Antivirus Pro offers three tiers: Free (100 req/hour), Pro (1000 req/hour), and Enterprise (10000 req/hour). The gateway extracts the user ID from the JWT, looks up their tier from the database, and applies the corresponding rate limit.

flowchart LR
    Client["Authenticated\nRequest"] --> Auth["Extract User\nfrom JWT/API Key"]
    Auth --> Tier["Lookup Tier\nfrom Database"]
    Tier --> RL["Apply Tier\nRate Limit"]
    RL -->|"Free: 100/hr"| Free["Allow"]
    RL -->|"Pro: 1000/hr"| Pro["Allow"]
    RL -->|"Enterprise: 10000/hr"| Enterprise["Allow"]
    style Auth fill:#dbeafe,stroke:#2563eb

Extracting User Identity

from flask import Flask, request, g
import jwt
import redis

app = Flask(__name__)
redis_client = redis.Redis(host="redis", port=6379, db=0)

@app.before_request
def identify_user():
    # Try JWT authentication
    auth_header = request.headers.get("Authorization", "")
    if auth_header.startswith("Bearer "):
        token = auth_header[7:]
        try:
            payload = jwt.decode(token, options={"verify_signature": False})
            g.user_id = payload.get("sub")
            g.user_tier = payload.get("tier", "free")
        except:
            g.user_id = None
            g.user_tier = "anonymous"
    else:
        g.user_id = None
        g.user_tier = "anonymous"

Tiered Rate Limiting

TIER_LIMITS = {
    "anonymous": {"limit": 10, "window": 3600},     # 10 req/hour
    "free": {"limit": 100, "window": 3600},          # 100 req/hour
    "pro": {"limit": 1000, "window": 3600},          # 1000 req/hour
    "enterprise": {"limit": 10000, "window": 3600},  # 10000 req/hour
}

def check_user_rate_limit():
    if not g.user_id:
        return True, "unlimited"

    tier_config = TIER_LIMITS.get(g.user_tier, TIER_LIMITS["free"])
    limit = tier_config["limit"]
    window = tier_config["window"]

    key = f"user:{g.user_id}:{g.user_tier}"
    now = int(__import__("time").time())
    window_key = now - (now % window)
    counter_key = f"rl:{key}:{window_key}"

    count = redis_client.incr(counter_key)
    if count == 1:
        redis_client.expire(counter_key, window * 2)

    g.rate_limit = limit
    g.rate_limit_remaining = max(0, limit - count)
    g.rate_limit_reset = window_key + window

    if count > limit:
        return False, "rate_limit_exceeded"
    return True, "ok"

Per-Endpoint User Limits

ENDPOINT_LIMITS = {
    "/api/scan": {"limit": 50, "window": 3600, "tier_overrides": {
        "enterprise": {"limit": 500, "window": 3600}
    }},
    "/api/reports": {"limit": 10, "window": 3600},
    "/api/export": {"limit": 5, "window": 3600},
}

def check_endpoint_rate_limit():
    path = request.path
    if path not in ENDPOINT_LIMITS:
        return True

    endpoint_config = ENDPOINT_LIMITS[path]
    limit = endpoint_config["limit"]
    window = endpoint_config["window"]

    # Check tier override
    tier_override = endpoint_config.get("tier_overrides", {})
    if g.get("user_tier") in tier_override:
        limit = tier_override[g.user_tier]["limit"]

    key = f"endpoint:{g.user_id}:{path}"
    count = redis_client.incr(f"rl:{key}:{int(time.time()) // window}")
    return count <= limit

Rate Limit Reset Per Tier

def get_rate_limit_for_user(user_id):
    tier = get_user_tier(user_id)
    config = TIER_LIMITS.get(tier, TIER_LIMITS["free"])
    key = f"user:{user_id}:{tier}"
    now = int(time.time())
    window_key = now - (now % config["window"])
    count = int(redis_client.get(f"rl:{key}:{window_key}") or 0)

    return {
        "tier": tier,
        "limit": config["limit"],
        "remaining": max(0, config["limit"] - count),
        "reset": window_key + config["window"],
    }

Common Mistakes

1. Not Handling Unauthenticated Users

Anonymous users must still be rate limited. Apply separate (lower) limits for unauthenticated traffic.

2. Storing User Tiers in the Token

If tiers change (user upgrades), the old token has the old tier. Store tier in database and check on each request, or use short-lived tokens.

3. Not Isolating Limits Per Endpoint

A user calling /scan 100 times should not exhaust their limit for /reports. Use per-endpoint counters.

4. Forgetting About Refresh Tokens

When a user refreshes their token, ensure the rate limit counter uses the same user ID, not a new session ID.

5. Over-Limiting Internal Admin Users

Admin users and internal services should have separate, much higher limits. Never apply customer-level limits to admin traffic.

Practice Questions

  1. How does user-based rate limiting differ from IP-based?
  2. Why should user tiers be checked from a database rather than the token?
  3. What limits should apply to anonymous (unauthenticated) users?
  4. How do you handle per-endpoint rate limits for users?
  5. Why should admin users have separate rate limits?

Answers:

  1. User-based limits track the authenticated account regardless of IP. IP limits track the source address regardless of user.
  2. Tiers can change (upgrade/downgrade) between token issuances. Database check ensures current limits are enforced.
  3. Anonymous users should have very low limits (10-50 req/hour) to prevent abuse while allowing legitimate testing.
  4. Maintain separate counters per user per endpoint. Each endpoint path gets its own key in Redis.
  5. Admin operations (user management, configuration changes) should not be rate limited by customer tiers. Apply generous administrative limits.

Challenge: Design a user rate limiting system for a SaaS API with three tiers. Include per-endpoint limits (some endpoints cost 5x the rate limit units), burst allowances, and a grace period for tier upgrades.

FAQ

How do you handle rate limit resets for different subscription tiers?

: Each tier has its own window. Free users reset hourly, enterprise daily. The window is calculated from the tier configuration.

Can users increase their rate limit without upgrading?

: No. Rate limits are enforced by the gateway based on the tier. Upgrading requires a subscription change.

How do you notify users about rate limit usage?

: Include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Send email alerts when usage exceeds 80%.

Should rate limits reset at the user's local midnight?

: Use UTC midnight consistently for all users to simplify implementation and avoid timezone confusion.

How do you handle rate limit spikes from legitimate user workflows?

: Allow bursts (token bucket) or provide batch endpoints that count as fewer rate limit units.

Mini Project

Build a user-based rate limiter with three tiers: anonymous (10 req/hour), basic (100 req/hour), premium (1000 req/hour). Use JWT authentication. Apply per-endpoint limits where /export costs 10 rate limit units per call. Return tier and remaining limits in headers.

What's Next

Continue with API Key Rate Limiting for partner and service account management, or explore Rate Limit Headers for client communication.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro