API Rate Limiting with Redis and PostgreSQL

Shivansh Jaiswal's photo
·

5 min read

A rate limit is a mechanism that restricts the number of requests a client can make to a server within a specific time frame, preventing abuse and ensuring fair resource usage.

Top 3 Benefits of Using a Rate Limiter in an App

  1. Protect Resources: Prevents resource exhaustion and ensures fair usage.

  2. Mitigate Abuse: Safeguards against malicious attacks and spam.

  3. Improve Performance: Optimises resource utilisation and enhances overall system performance.

Lets implements fixed-window rate limiting using Redis for real-time tracking and PostgreSQL for configuration management. By applying fixed-time windows, we limit the number of requests that a user can make within specific intervals, enabling precise control over usage patterns and protecting against abuse.

System Components

  • PostgreSQL: Stores rate limit configurations per user and API endpoint.

  • Redis: Tracks API usage in real-time with a TTL-based structure.

  • Annotations: Assigns aliases to API endpoints, allowing flexible configuration by mapping multiple endpoints to the same alias.

    This combination provides both efficient tracking and easy configurability for scalable, fine-grained rate limiting.

PostgreSQL for Rate Limit Configurations

PostgreSQL stores rate limit settings per user and API alias, simplifying configuration management. We define the following tables:

  • users: Stores user information (id, email, etc).

  • api_identifier: Assigns unique aliases to API endpoints and tracks endpoint details.

  • api_rate_limit_config: Links specific rate limits to each user and API alias.

    • user_id: Links to users.id.

    • api_alias: Alias assigned to the endpoint via annotations.

    • time_window_in_sec: Duration of the time window.

    • limit: Maximum allowed requests within the time window.

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);

CREATE TABLE api_identifier (
    id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    name VARCHAR(255) NOT NULL,  -- A unique alias for each API (used in annotations)
    endpoint_path VARCHAR(255) NOT NULL,  -- The actual path of the API endpoint
    http_method VARCHAR(10) NOT NULL,  -- HTTP method (GET, POST, PUT, DELETE)
    description TEXT                    -- Optional description of the API endpoint
);

CREATE TABLE api_rate_limit_config (
    id SERIAL PRIMARY KEY,
    api_id BIGINT REFERENCES api_identifier(id) NOT NULL,
    time_window_in_sec INT NOT NULL,
    limit INT NOT NULL,
    UNIQ ( api_id, time_window_in_sec )
);

Redis for Real-Time Rate Tracking

  • Redis tracks requests in real time using a TTL-based key structure. Keys are defined as user_id:api_id:time_window, allowing efficient usage tracking per endpoint per user.

    • Tracks the number of requests made in the current time window.

    • Each key has a TTL set to time_window_in_sec

  • Redis Commands:

    • Check if key exists: EXISTS user_id:api_id:time_window

    • Increment usage count: INCR user_id:api_id:time_window

    • Set TTL for time-based reset: EXPIRE user_id:api_id:time_window time_window_in_sec

Annotations for Endpoint Aliasing

Annotations are often used in languages like Java and Python to simplify functionality and make code cleaner and more modular. They allow you to define behaviours that are then applied or "triggered" at runtime.

Annotations provide a flexible mechanism to alias API endpoints, enabling administrators to group functionally similar endpoints under a single alias. This approach simplifies rate limit configuration and management, as changes to the rate limit for an alias apply across all endpoints sharing it.

Here’s how to create and use annotations for rate limiting in Java and Python.

  1. Define Annotation: Use a custom annotation, e.g., @ApiIdentifier("GET_USER_INFORMATION"), to specify an alias for each endpoint or method in the controller.

     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
     public @interface ApiIdentifier {
         String value(); // to store the identifier like "GET_USER_INFORMATION"
     }
    
     from functools import wraps
    
     def api_identifier(identifier):
         def decorator(view_func):
             @wraps(view_func)
             def wrapped_view(*args, **kwargs):
                 # Print the identifier for demonstration
                 print(f"API Identifier: {identifier}")
                 return view_func(*args, **kwargs)
             wrapped_view.api_identifier = identifier  # Attach the identifier to the view
             return wrapped_view
         return decorator
    
  2. Apply Annotation: Assign a unique alias to each endpoint. This alias is then used as the api_identifier.name in both PostgreSQL and Redis.

     @RestController
     public class UserController {
    
         @ApiIdentifier("GET_USER_INFORMATION")
         @GetMapping("/api/user/info")
         public String getUserInfo() {
             return "User information data";
         }
     }
    
    
     from django.http import JsonResponse
     from django.views import View
    
     # Function-based view example
     @api_identifier("GET_USER_INFORMATION")
     def get_user_info(request):
         return JsonResponse({"message": "User information data"})
    
  3. Configuration Retrieval: On each request, look up the api_id in api_rate_limit_config to fetch the appropriate rate limit.

     @Component
     public class ApiIdentifierInterceptor implements HandlerInterceptor {
    
         @Override
         public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
             if (handler instanceof HandlerMethod) {
                 Method method = ((HandlerMethod) handler).getMethod();
                 ApiIdentifier apiIdentifier = method.getAnnotation(ApiIdentifier.class);
                 if (apiIdentifier != null) {
                     String identifier = apiIdentifier.value();
                     System.out.println("API Identifier: " + identifier);
                     // Use identifier for logging, rate limiting, etc.
                 }
             }
             return true;
         }
     }
    
     from django.utils.deprecation import MiddlewareMixin
    
     class ApiIdentifierMiddleware(MiddlewareMixin):
    
         def process_view(self, request, view_func, view_args, view_kwargs):
             # Check if the view has an `api_identifier` attribute
             identifier = getattr(view_func, 'api_identifier', None)
             if identifier:
                 print(f"API Identifier: {identifier}")
                 # Here, you could implement rate limiting, logging, etc., based on identifier.
             return None
    
  4. Redis Validation: Check Redis using the alias-based key structure. Increment the request count or deny access if the rate limit is reached.

Response Header

  • To inform clients of their usage and reset times, include the following headers in the response:

    • X-RateLimit-Limit: Shows the maximum number of requests allowed within the time window.

    • X-RateLimit-Remaining: Indicates the remaining requests in the current window.

    • X-RateLimit-Reset: Provides the timestamp when the rate limit will reset, in Unix epoch seconds.

Summary

This setup combines Redis’s efficiency in real-time usage tracking with PostgreSQL’s reliability for storing configuration. By using annotations for aliasing, you can manage multiple endpoints under shared limits, creating a powerful, flexible rate-limiting solution that scales easily:

  • Redis: Efficiently tracks request counts and resets automatically per time_window_in_sec.

  • PostgreSQL: Holds rate limit settings per user and API alias, making configurations easily manageable.

  • Annotations: Streamline rate-limiting by mapping each API endpoint to an alias, allowing consistent configurations across the system.