Skip to content

Repository files navigation

Rate Limiter

A distributed, low-latency rate limiting service built with Java 21, Spring Boot, Redis, and Lua.

The service provides two rate-limiting algorithms:

  • Fixed Window Counter
  • Sliding Window

Rate-limit state is stored in Redis and the critical counter/window operations are executed atomically using Redis Lua scripts, making the service suitable for distributed deployments where multiple application instances share the same rate-limit state.


Table of Contents


Introduction

Rate limiting is a fundamental mechanism for protecting APIs and backend services from excessive traffic.

Without rate limiting, a single client can potentially generate enough requests to:

  • Exhaust server resources
  • Increase database load
  • Degrade service performance
  • Abuse expensive APIs
  • Cause cascading failures
  • Affect other users of the system

This project implements a standalone rate-limiting service that can be placed in front of backend APIs or used as a shared infrastructure component.

The primary goal is to explore how rate limiting works at the system-design and implementation level, particularly in a distributed environment where multiple application instances need to make consistent rate-limit decisions.


Overview

The service exposes HTTP APIs where a caller provides:

  • An identity
  • A request limit
  • A time window

For example:

{
  "identity": "user:1234",
  "limit": 5,
  "windowSeconds": 20
}

This configuration means:

Allow up to 5 requests for user:1234 within a 20-second window.

Once the configured quota has been exhausted, subsequent requests receive:

HTTP/1.1 429 Too Many Requests

The response also provides information about when the client can retry.

The application runs on port 8081 and uses the context path:

/api/v1/rate-limiter

The default configuration is defined in application.yaml.


Features

Rate Limiting

  • Fixed Window Counter
  • Sliding Window
  • Per-identity rate limits
  • Configurable request limits
  • Configurable time windows

Distributed State

  • Redis-backed state
  • Shared rate-limit state across application instances
  • Suitable for horizontally scaled deployments

Atomic Operations

Rate-limit decisions are executed inside Redis Lua scripts.

This keeps operations such as:

Read → Check → Increment → Expire

inside a single Redis-side operation.

HTTP Semantics

  • 200 OK when a request is allowed
  • 429 Too Many Requests when the limit is exceeded
  • Retry-After response header for rejected requests

Validation

The API validates:

  • Identity must not be blank
  • Limit must be greater than zero
  • Window duration must be greater than zero

The request contract is implemented using Jakarta Bean Validation.

Automated Integration Tests

The repository contains Python-based integration tests covering:

  • Normal requests
  • Quota exhaustion
  • 429 responses
  • Retry timing
  • Window expiration
  • Concurrent requests
  • Race conditions
  • Independent identities
  • Sliding-window behavior
  • Boundary conditions

Architecture

                    ┌──────────────────────┐
                    │       Client         │
                    │  API / Service / App │
                    └──────────┬───────────┘
                               │
                               │ HTTP
                               ▼
                  ┌──────────────────────────┐
                  │    Rate Limiter API      │
                  │                          │
                  │   Spring Boot / Java 21  │
                  └────────────┬─────────────┘
                               │
                               ▼
                  ┌──────────────────────────┐
                  │    RateLimitService      │
                  └────────────┬─────────────┘
                               │
                  ┌────────────┴────────────┐
                  │                         │
                  ▼                         ▼
        ┌──────────────────┐     ┌──────────────────┐
        │ Fixed Window     │     │ Sliding Window   │
        │ Strategy         │     │ Strategy         │
        └────────┬─────────┘     └────────┬─────────┘
                 │                        │
                 └───────────┬────────────┘
                             ▼
                 ┌─────────────────────────┐
                 │ RedisRateLimitRepository│
                 └────────────┬────────────┘
                              │
                    Redis Lua Scripts
                              │
                              ▼
                 ┌─────────────────────────┐
                 │         Redis           │
                 │                         │
                 │  Shared Rate-Limit State│
                 └─────────────────────────┘

The Java implementation separates the HTTP controllers, service layer, rate-limiting strategies, and Redis persistence layer.


Rate Limiting Algorithms

Fixed Window

The Fixed Window algorithm divides time into discrete windows.

For example:

Limit: 5 requests
Window: 20 seconds

The timeline looks like:

|--------- 20 seconds ---------|
0                             20

Requests:
R1 R2 R3 R4 R5
               └── allowed

R6
└── rejected

When the next window begins, the quota resets.

Implementation

The Lua script calculates the current window using:

window = floor(currentTime / windowSeconds)

The Redis key is then constructed as:

rate-limit:fixed:<identity>:<window>

Each request increments the counter for the current window.

If:

currentCount <= limit

the request is allowed.

Otherwise it is rejected.

Advantages

  • Simple
  • Low memory usage
  • Fast
  • Easy to reason about
  • Efficient Redis representation

Disadvantage

Fixed windows can produce a boundary burst.

For example, a client may consume its entire quota at the end of one window and immediately consume another full quota at the beginning of the next window.

Window 1              Window 2
|---------------------|---------------------|

       5 requests     |     5 requests
          allowed     |        allowed

This can result in a burst of 10 requests over a period much shorter than the configured window.


Sliding Window

The Sliding Window algorithm evaluates requests over the continuously moving interval:

[now - windowSeconds, now]

Instead of resetting the entire quota at a fixed boundary, individual requests expire as they move outside the window.

For example:

Limit = 5
Window = 20 seconds

If five requests arrive:

t=0
t=2
t=4
t=6
t=8

the quota becomes exhausted.

At:

t=20

the first request expires.

One slot becomes available.

At:

t=22

the second request expires.

Another slot becomes available.

This produces gradual quota recovery.

Redis Implementation

The Sliding Window implementation uses a Redis Sorted Set.

Each request is stored with a timestamp as its score.

The algorithm:

  1. Gets the current Redis server time.
  2. Calculates the start of the sliding window.
  3. Removes requests older than the window.
  4. Counts the remaining requests.
  5. Rejects the request if the count has reached the limit.
  6. Otherwise inserts the request into the sorted set.
  7. Refreshes the Redis key expiration.

The Redis key follows:

rate-limit:sliding:<identity>

The Lua script uses microsecond-resolution timestamps so requests occurring within the same second remain correctly ordered.

Advantages

  • More accurate than fixed windows
  • Prevents fixed-window boundary bursts
  • Quota becomes available gradually
  • Better representation of real request traffic

Trade-off

The Sliding Window requires storing individual request entries in a Redis Sorted Set, so it consumes more memory than the Fixed Window counter.


Technology Stack

Technology Purpose
Java 21 Application runtime
Spring Boot Application framework
Spring MVC REST API
Spring Data Redis Redis integration
Redis Distributed state store
Redis Lua Atomic rate-limit operations
Maven Build and dependency management
Docker Containerization
Docker Compose Local multi-container environment
Python Integration testing

The Maven configuration currently targets Java 21 and includes Spring Boot, Redis, validation, MVC, and Actuator dependencies.


Project Structure

Rate-Limiter/
│
├── .mvn/
│   └── wrapper/
│
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── dev/
│   │   │       └── suwe/
│   │   │           ├── controller/
│   │   │           │   ├── FixedWindowController.java
│   │   │           │   └── SlidingWindowController.java
│   │   │           │
│   │   │           ├── database/
│   │   │           │   └── redis/
│   │   │           │       ├── RedisConfig.java
│   │   │           │       ├── RedisLuaResult.java
│   │   │           │       └── RedisRateLimitRepository.java
│   │   │           │
│   │   │           ├── dto/
│   │   │           │   ├── RateLimitRequest.java
│   │   │           │   └── RateLimitResponse.java
│   │   │           │
│   │   │           ├── exception/
│   │   │           │
│   │   │           ├── service/
│   │   │           │   └── RateLimitService.java
│   │   │           │
│   │   │           ├── strategy/
│   │   │           │   ├── RateLimitStrategy.java
│   │   │           │   ├── FixedWindowStrategy.java
│   │   │           │   └── SlidingWindowStrategy.java
│   │   │           │
│   │   │           └── RateLimiterApplication.java
│   │   │
│   │   └── resources/
│   │       ├── scripts/
│   │       │   ├── fixed-window.lua
│   │       │   └── sliding-window.lua
│   │       │
│   │       └── application.yaml
│   │
│   └── test/
│       └── java/
│
├── test_fixed_window.py
├── test_sliding_window.py
│
├── Dockerfile
├── docker-compose.yml
├── pom.xml
├── mvnw
└── mvnw.cmd

Prerequisites

Required

Java

Java 21 or later.

Verify:

java -version

Maven

Maven is not strictly required because the repository includes the Maven Wrapper.

You can use:

./mvnw

instead of installing Maven globally.

Redis

Redis is required for the rate limiter.

Verify:

redis-cli ping

Expected:

PONG

Docker

For the easiest setup:

  • Docker
  • Docker Compose

The repository already contains both a Dockerfile and docker-compose.yml.

Python

Python is required only if you want to run the integration test suites.

The tests use the requests package.

Install it with:

pip install requests

Getting Started

1. Clone the Repository

git clone https://github.com/suwe-dev/Rate-Limiter.git
cd Rate-Limiter

2. Run with Docker Compose

The easiest way to start the complete application is:

docker compose up --build -d

This starts:

rate-limiter-app
        │
        ▼
redis-server

The application is exposed on:

http://localhost:8081

Redis runs internally on the Docker network and does not need to be exposed to the host.

The Compose configuration connects the application to Redis using:

SPRING_DATA_REDIS_HOST=redis-server
SPRING_DATA_REDIS_PORT=6379

and exposes the application on port 8081.

Check running containers:

docker compose ps

View application logs:

docker compose logs -f app

Stop the application:

docker compose down

3. Run Locally

Start Redis:

redis-server

Then start the Spring Boot application:

./mvnw spring-boot:run

The application will start on:

http://localhost:8081

with the base context:

/api/v1/rate-limiter

The default Redis configuration expects:

localhost:6379

as configured in application.yaml.


API Reference

Base URL

http://localhost:8081/api/v1/rate-limiter

Request Format

Both algorithms accept the same request body:

{
  "identity": "user:1234",
  "limit": 5,
  "windowSeconds": 20
}

Fields

Field Type Description
identity String Unique identifier being rate limited
limit Integer Maximum number of allowed requests
windowSeconds Long Duration of the rate-limit window

The identity can represent almost anything meaningful to the application:

user:1234
api-key:abc123
ip:192.168.1.10
tenant:acme
service:payment

Fixed Window Endpoint

POST /api/v1/rate-limiter/fixed-window

Example

curl -X POST \
  http://localhost:8081/api/v1/rate-limiter/fixed-window \
  -H "Content-Type: application/json" \
  -d '{
    "identity": "user:1234",
    "limit": 5,
    "windowSeconds": 20
  }'

Sliding Window Endpoint

POST /api/v1/rate-limiter/sliding-window

Example

curl -X POST \
  http://localhost:8081/api/v1/rate-limiter/sliding-window \
  -H "Content-Type: application/json" \
  -d '{
    "identity": "user:1234",
    "limit": 5,
    "windowSeconds": 20
  }'

Successful Response

When a request is accepted:

HTTP/1.1 200 OK

Example:

{
  "allow": true,
  "reason": null,
  "retryAfterSeconds": null
}

For the Sliding Window implementation, the underlying Lua script returns the number of remaining slots on an allowed request. The current Java response model exposes this value through the retryAfterSeconds field, so consumers should treat the field according to the algorithm's current implementation rather than assuming it always means "retry after".


Rate Limit Exceeded

Once the limit is reached:

HTTP/1.1 429 Too Many Requests
Retry-After: 17

Example response:

{
  "allow": false,
  "reason": "Rate limit exceeded",
  "retryAfterSeconds": 17
}

The controllers explicitly return HTTP 429 and populate the standard Retry-After header when the request is rejected.


Validation

Invalid requests are rejected through Jakarta Bean Validation.

Empty identity

{
  "identity": "",
  "limit": 5,
  "windowSeconds": 20
}

Invalid because identity must not be blank.

Zero limit

{
  "identity": "user:1234",
  "limit": 0,
  "windowSeconds": 20
}

Invalid because the limit must be greater than zero.

Zero window

{
  "identity": "user:1234",
  "limit": 5,
  "windowSeconds": 0
}

Invalid because windowSeconds must be greater than zero.


Testing

The project includes dedicated Python integration test suites.

Install Test Dependency

pip install requests

Make sure the application and Redis are already running.


Fixed Window Tests

Run:

python test_fixed_window.py

The test suite validates:

  • Requests within quota return 200
  • Requests beyond quota return 429
  • Retry-After is returned
  • retryAfterSeconds behaves correctly
  • Window expiration restores the quota
  • Concurrent requests do not exceed the configured limit

The fixed-window suite targets:

http://localhost:8081/api/v1/rate-limiter/fixed-window

and uses a default test configuration of:

10 requests / 30 seconds

Sliding Window Tests

Run:

python test_sliding_window.py

The Sliding Window suite performs more extensive behavioral testing, including:

  • Requests within quota
  • Quota exhaustion
  • HTTP 429
  • Retry timing
  • Gradual quota recovery
  • Fixed-window boundary burst prevention
  • Window-edge behavior
  • Concurrent requests
  • Race-condition checks
  • Independent identities
  • Tiny-window behavior
  • Invalid limits

How It Works

A typical request flows through the service as follows:

1. Client
     │
     ▼
2. REST Controller
     │
     ▼
3. RateLimitService
     │
     ▼
4. RateLimitStrategy
     │
     ▼
5. RedisRateLimitRepository
     │
     ▼
6. Redis Lua Script
     │
     ▼
7. Atomic rate-limit decision
     │
     ▼
8. HTTP 200 / 429

The service layer delegates the request to the appropriate strategy, while the strategies delegate Redis operations to the repository.

This separation makes the algorithm implementation independent from the HTTP layer.


Distributed Rate Limiting

A local in-memory rate limiter works only when all requests for an identity reach the same application instance.

Consider:

                Load Balancer
                     │
          ┌──────────┼──────────┐
          ▼          ▼          ▼
       Server A   Server B   Server C
          │          │          │
          └──────────┼──────────┘
                     ▼
                   Redis

If each server maintains its own counter:

Server A → 5 requests
Server B → 5 requests
Server C → 5 requests

a configured limit of 5 could accidentally become:

15 requests

because each instance has independent state.

This project moves the state into Redis:

Server A ──┐
Server B ──┼──► Redis
Server C ──┘

All application instances therefore evaluate the same identity against the same shared state.

This is the key property that makes the implementation suitable for distributed deployments.


Concurrency and Atomicity

Rate limiting is inherently sensitive to race conditions.

Consider two requests arriving simultaneously:

Request A ──┐
            ├── Check count → Increment
Request B ──┘

If the operations are performed independently, both requests may observe the same old count before either increment occurs.

This can result in:

Configured limit: 10

Actual successful requests: 11

or more under heavy concurrency.

This project addresses the critical operation using Redis Lua scripts.

The rate-limit decision is performed inside Redis rather than distributing the individual operations across multiple network calls.

For Fixed Window, the Lua script performs the counter increment and decision together.

For Sliding Window, cleanup, counting, decision, and insertion are executed by the Lua script.

This is also why the integration tests explicitly include concurrent burst scenarios and race-condition checks.


Redis Data Model

Fixed Window

Keys follow:

rate-limit:fixed:<identity>:<window>

Example:

rate-limit:fixed:user:1234:88234567

The value is a request counter.

The key receives a TTL corresponding to the remaining time in the current window.


Sliding Window

Keys follow:

rate-limit:sliding:<identity>

The value is a Redis Sorted Set.

Conceptually:

Key:
rate-limit:sliding:user:1234

Sorted Set:

timestamp        request
---------        ----------------
1725000000.10    request-a
1725000000.52    request-b
1725000001.14    request-c
1725000002.91    request-d

The timestamp is used as the score.

When a request arrives, entries older than the current sliding window are removed before calculating the current request count.


Configuration

Default application configuration:

server:
  port: 8081
  servlet:
    context-path: /api/v1/rate-limiter

spring:
  application:
    name: rate-limiter

  data:
    redis:
      host: localhost
      port: 6379
      timeout: 2s

When running through Docker Compose, the Redis host is changed to:

redis-server

because the application communicates with Redis through the Docker network.


Docker

The Docker image uses a multi-stage build.

Build stage

Maven + Eclipse Temurin 21

The application is compiled and packaged into a JAR.

Runtime stage

Eclipse Temurin 21 JRE Alpine

The final container runs using a non-root user:

appuser

and exposes:

8081

This keeps the runtime image smaller and avoids running the application as root.


Docker Compose Architecture

The Compose configuration defines two services:

┌─────────────────────────┐
│ rate-limiter-app        │
│                         │
│ Spring Boot             │
│ Port 8081               │
└────────────┬────────────┘
             │
             │ Docker Network
             ▼
┌─────────────────────────┐
│ redis-server            │
│                         │
│ Redis 8.2 Alpine        │
│ Port 6379               │
└─────────────────────────┘

Redis is connected to the application through the internal Docker network.


Design Decisions

Why Redis?

Redis is a natural fit for rate limiting because it provides:

  • Low-latency reads and writes
  • Atomic operations
  • TTL support
  • Sorted Sets
  • Lua scripting
  • Shared state between application instances

Why Lua?

Without Lua, a rate-limit operation could require multiple Redis commands:

GET
CHECK
INCR
EXPIRE

Multiple commands introduce opportunities for race conditions between concurrent clients.

Lua allows the complete decision-making operation to execute within Redis as one server-side script.


Why Strategy Pattern?

The application defines:

RateLimitStrategy

with concrete implementations:

FixedWindowStrategy
SlidingWindowStrategy

This keeps the algorithms isolated from the service and controller layers.

Adding another algorithm can therefore be done without rewriting the HTTP API layer.

Potential future strategies could include:

TokenBucketStrategy
LeakyBucketStrategy

Fixed Window vs Sliding Window

Property Fixed Window Sliding Window
Implementation Counter Sorted Set
Memory usage Low Higher
Complexity Low Higher
Boundary burst Possible Prevented
Quota recovery All at once Gradual
Redis structure String/Counter Sorted Set
Best for Simple quotas More accurate traffic control
Lua required Yes Yes

Example Use Cases

API Protection

POST /payments

Limit:
10 requests / 60 seconds

Prevents a client from repeatedly calling an expensive payment endpoint.


User-Level Rate Limiting

{
  "identity": "user:1234",
  "limit": 100,
  "windowSeconds": 60
}

Each user receives an independent quota.


API-Key Rate Limiting

{
  "identity": "api-key:customer-abc",
  "limit": 1000,
  "windowSeconds": 3600
}

Useful for public APIs with customer-specific quotas.


Tenant-Level Rate Limiting

{
  "identity": "tenant:acme",
  "limit": 5000,
  "windowSeconds": 60
}

Useful in multi-tenant backend systems.


Limitations

This project is primarily an implementation and learning project rather than a complete production gateway.

Current limitations include:

  • No authentication or authorization
  • No API-key management
  • No persistent rate-limit policy configuration
  • Rate-limit configuration is supplied by the caller
  • No built-in distributed configuration management
  • No metrics dashboard
  • No circuit-breaker integration
  • No automatic client identification
  • No policy management UI
  • No built-in service discovery
  • No configurable fail-open/fail-closed behavior for Redis outages

For a production deployment, rate-limit policies would typically be controlled by the service rather than allowing arbitrary clients to supply their own limits.


Future Improvements

Possible extensions include:

Additional Algorithms

  • Token Bucket
  • Leaky Bucket
  • Generic Cell Rate Algorithm

Policy Management

Instead of accepting the limit from every request:

{
  "identity": "user:1234"
}

the service could resolve policies internally:

user:1234
     │
     ▼
Policy Service
     │
     ├── limit = 100
     └── window = 60s

Response Headers

Add standardized headers such as:

X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset

Observability

Add:

  • Prometheus metrics
  • Grafana dashboards
  • Request latency metrics
  • Allowed/rejected request counters
  • Redis operation metrics
  • Algorithm-specific metrics

High Availability

Introduce:

  • Redis Sentinel
  • Redis Cluster
  • Multiple application replicas
  • Load balancing

Configuration

Move configuration into:

  • Environment variables
  • Spring profiles
  • External configuration
  • Centralized policy storage

Learning Objectives

This project demonstrates several backend and distributed-system concepts:

  • Rate limiting algorithms
  • Fixed Window vs Sliding Window
  • Redis data structures
  • Redis Sorted Sets
  • Redis TTLs
  • Redis Lua scripting
  • Atomic operations
  • Race-condition prevention
  • Distributed shared state
  • Spring Boot REST APIs
  • Strategy Pattern
  • Docker containerization
  • Docker Compose
  • Integration testing
  • Concurrent request testing
  • HTTP 429 Too Many Requests
  • Retry-After semantics

Running the Complete Project

The shortest path to run everything:

1. Start the application

docker compose up --build -d

2. Verify containers

docker compose ps

3. Test Fixed Window

python test_fixed_window.py

4. Test Sliding Window

python test_sliding_window.py

5. Stop everything

docker compose down

Example Request

curl -X POST \
  http://localhost:8081/api/v1/rate-limiter/sliding-window \
  -H "Content-Type: application/json" \
  -d '{
    "identity": "user:1234",
    "limit": 5,
    "windowSeconds": 20
  }'

Possible response:

{
  "allow": true,
  "reason": null,
  "retryAfterSeconds": 4
}

After the quota is exhausted:

{
  "allow": false,
  "reason": "Rate limit exceeded",
  "retryAfterSeconds": 17
}

with:

HTTP/1.1 429 Too Many Requests
Retry-After: 17

Conclusion

This project implements a distributed rate-limiting service using Spring Boot, Redis, and Lua, with a focus on correctness under concurrent traffic.

The key architectural idea is simple:

Application instances
        │
        ▼
    Redis
        │
        ▼
Atomic Lua operation
        │
        ▼
Allow / Reject

The Fixed Window implementation provides a lightweight counter-based approach, while the Sliding Window implementation provides more precise control by tracking individual request timestamps.

Together, they provide a practical exploration of how rate limiting can be implemented as a shared backend service rather than as local application state.


License

This project is licensed under the MIT License.

You are free to use, copy, modify, distribute, and use this project for personal or commercial purposes, subject to the terms of the license.


Author

Suwethan M

GitHub: https://github.com/suwe-dev

About

A high-performance distributed rate limiting service built with Redis and Lua, supporting fixed-window rate limiting with atomic operations and low-latency request processing.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages