{
  "id": "java-lettuce",
  "title": "Token bucket rate limiter with Redis and Java (Lettuce)",
  "url": "https://redis.io/docs/latest/develop/use-cases/rate-limiter/java-lettuce/",
  "summary": "Implement a token bucket rate limiter using Redis and Lua scripts in Java with Lettuce",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-04-16T13:29:55-07:00",
  "children": [],
  "page_type": "content",
  "content_hash": "2fedd05e3c532791ab78803908b472d2e56c094cad7a4d98280c40d95eaf7141",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide shows you how to implement a distributed token bucket rate limiter using Redis and Lua scripts in Java with the [`Lettuce`](https://redis.io/docs/latest/develop/clients/lettuce) client library."
    },
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Rate limiting is a critical technique for controlling the rate at which operations are performed. Common use cases include:\n\n* Limiting API requests per user or IP address\n* Preventing abuse and protecting against denial-of-service attacks\n* Ensuring fair resource allocation across multiple clients\n* Throttling background jobs or batch operations\n\nThe **token bucket algorithm** is a popular rate limiting approach that allows bursts of traffic while maintaining an average rate limit over time. This guide covers the Java implementation using the [`Lettuce`](https://redis.io/docs/latest/develop/clients/lettuce) client library, taking advantage of Lettuce's `RedisClient` for connection management, `StatefulRedisConnection` for multiplexed connections, and support for synchronous, asynchronous, and reactive command execution."
    },
    {
      "id": "how-it-works",
      "title": "How it works",
      "role": "content",
      "text": "The token bucket algorithm works like a bucket that holds tokens:\n\n1. **Initialization**: The bucket starts with a maximum capacity of tokens\n2. **Refill**: Tokens are added to the bucket at a constant rate (for example, 1 token per second)\n3. **Consumption**: Each request consumes one token from the bucket\n4. **Decision**: If tokens are available, the request is allowed; otherwise, it's denied\n5. **Capacity limit**: The bucket never exceeds its maximum capacity\n\nThis approach allows for burst traffic (using accumulated tokens) while enforcing an average rate limit over time."
    },
    {
      "id": "why-use-redis",
      "title": "Why use Redis?",
      "role": "content",
      "text": "Redis is ideal for distributed rate limiting because:\n\n* **Atomic operations**: Lua scripts execute atomically, preventing race conditions\n* **Shared state**: Multiple application servers can share the same rate limit counters\n* **High performance**: In-memory operations provide microsecond latency\n* **Automatic expiration**: Keys can be set to expire automatically (though not used in this implementation)"
    },
    {
      "id": "the-lua-script",
      "title": "The Lua script",
      "role": "content",
      "text": "The core of this implementation is a Lua script that runs atomically on the Redis server. This ensures that checking and updating the token bucket happens in a single operation, preventing race conditions in distributed environments.\n\nHere's how the script works:\n\n[code example]"
    },
    {
      "id": "script-breakdown",
      "title": "Script breakdown",
      "role": "content",
      "text": "1. **State retrieval**: Uses [`HMGET`](https://redis.io/docs/latest/commands/hmget) to fetch the current token count and last refill time from a hash\n2. **Initialization**: On first use, sets tokens to full capacity\n3. **Token refill calculation**: Computes how many tokens should be added based on elapsed time\n4. **Capacity enforcement**: Uses `math.min()` to ensure tokens never exceed capacity\n5. **Token consumption**: Decrements the token count if available\n6. **State update**: Uses [`HMSET`](https://redis.io/docs/latest/commands/hmset) to save the new state\n7. **Return value**: Returns both the decision (allowed/denied) and remaining tokens"
    },
    {
      "id": "why-atomicity-matters",
      "title": "Why atomicity matters",
      "role": "content",
      "text": "Without atomic execution, race conditions could occur:\n\n* **Double spending**: Two requests could read the same token count and both succeed when only one should\n* **Lost updates**: Concurrent updates could overwrite each other's changes\n* **Inconsistent state**: Token count and refill time could become desynchronized\n\nUsing [`EVAL`](https://redis.io/docs/latest/commands/eval) or [`EVALSHA`](https://redis.io/docs/latest/commands/evalsha) ensures the entire operation executes atomically, making it safe for distributed systems."
    },
    {
      "id": "installation",
      "title": "Installation",
      "role": "setup",
      "text": "Add the Lettuce dependency to your project:\n\n* If you use **Maven**:\n\n  [code example]\n\n* If you use **Gradle**:\n\n  [code example]"
    },
    {
      "id": "lettuce-vs-jedis",
      "title": "Lettuce vs Jedis",
      "role": "content",
      "text": "Unlike [`Jedis`](https://redis.io/docs/latest/develop/clients/jedis), which uses a connection pool (`JedisPool`) with one thread per connection, Lettuce uses a single `RedisClient` with multiplexed connections through `StatefulRedisConnection`. This means:\n\n* **No connection pool needed**: A single connection handles multiple concurrent requests\n* **Thread-safe by default**: The `StatefulRedisConnection` and its command interfaces are thread-safe\n* **Sync, async, and reactive**: You can choose the execution model that fits your application"
    },
    {
      "id": "using-the-java-class",
      "title": "Using the Java class",
      "role": "content",
      "text": "The `TokenBucket` class provides a thread-safe interface for rate limiting\n([source](TokenBucket.java)):\n\n[code example]\n\nNote that Lettuce takes a `StatefulRedisConnection` directly rather than a connection pool. The connection is multiplexed, so a single connection can safely handle concurrent requests from multiple threads. The `allow()` method returns a `RateLimitResult` record containing both the decision and the remaining token count."
    },
    {
      "id": "configuration-parameters",
      "title": "Configuration parameters",
      "role": "configuration",
      "text": "* **capacity**: Maximum number of tokens in the bucket (controls burst size)\n* **refillRate**: Number of tokens added per refill interval\n* **refillInterval**: Time in seconds between refills\n\nFor example:\n* `capacity=10, refillRate=1, refillInterval=1.0` allows 10 requests per second with bursts up to 10\n* `capacity=100, refillRate=10, refillInterval=1.0` allows 10 requests per second with bursts up to 100\n* `capacity=60, refillRate=1, refillInterval=60.0` allows 1 request per minute with bursts up to 60"
    },
    {
      "id": "rate-limit-keys",
      "title": "Rate limit keys",
      "role": "content",
      "text": "The `key` parameter identifies what you're rate limiting. Common patterns:\n\n* **Per user**: `user:{userId}` - Limit each user independently\n* **Per IP address**: `ip:{ipAddress}` - Limit by client IP\n* **Per API endpoint**: `api:{endpoint}:{userId}` - Different limits per endpoint\n* **Global**: `global:api` - Single limit shared across all requests"
    },
    {
      "id": "script-caching-with-evalsha",
      "title": "Script caching with EVALSHA",
      "role": "content",
      "text": "The Java implementation uses [`EVALSHA`](https://redis.io/docs/latest/commands/evalsha) for optimal performance. On first use, the Lua script is loaded into Redis using Lettuce's `scriptLoad()` command, and subsequent calls use the cached SHA1 hash. If the script is evicted from the cache, the class automatically falls back to [`EVAL`](https://redis.io/docs/latest/commands/eval) and reloads the script. The script SHA is computed once on first use and cached for subsequent calls.\n\n[code example]"
    },
    {
      "id": "thread-safety",
      "title": "Thread safety",
      "role": "content",
      "text": "The `TokenBucket` class is thread-safe because Lettuce's `StatefulRedisConnection` is inherently thread-safe and multiplexes commands over a single connection. You can share a single instance across your application:\n\n[code example]"
    },
    {
      "id": "asynchronous-usage",
      "title": "Asynchronous usage",
      "role": "content",
      "text": "Lettuce also supports asynchronous command execution. The async version of the rate limiter\n([source](AsyncTokenBucket.java)) returns a `CompletableFuture<RateLimitResult>`:\n\n[code example]"
    },
    {
      "id": "reactive-usage",
      "title": "Reactive usage",
      "role": "content",
      "text": "Lettuce's reactive API is useful when you are already using Reactor in a non-blocking application.\nThe reactive version of the rate limiter ([source](ReactiveTokenBucket.java)) returns a `Mono<RateLimitResult>`:\n\n[code example]"
    },
    {
      "id": "running-the-demo",
      "title": "Running the demo",
      "role": "content",
      "text": "A demonstration HTTP server is included to show the rate limiter in action\n([source](DemoServer.java)):\n\n[code example]\n\nThe demo provides an interactive web interface where you can:\n\n* Submit requests and see them allowed or denied in real-time\n* View the current token count\n* Adjust rate limit parameters dynamically\n* Test different rate limiting scenarios\n\nThe demo assumes Redis is running on `localhost:6379` but you can specify a different host and port using the `--redis-host HOST` and `--redis-port PORT` command-line arguments. Visit `http://localhost:8080` in your browser to try it out."
    },
    {
      "id": "response-headers",
      "title": "Response headers",
      "role": "returns",
      "text": "It's common to include rate limit information in HTTP response headers:\n\n[code example]"
    },
    {
      "id": "customization",
      "title": "Customization",
      "role": "content",
      "text": ""
    },
    {
      "id": "using-as-a-servlet-filter",
      "title": "Using as a servlet filter",
      "role": "content",
      "text": "You can wrap the rate limiter as a servlet filter for use with any Java web framework:\n\n[code example]\n\nNote that unlike the Jedis version, the Lettuce servlet filter includes a `destroy()` method to properly shut down the `RedisClient` and close the connection. This is important because Lettuce manages its own event loop threads that need to be cleaned up."
    },
    {
      "id": "error-handling",
      "title": "Error handling",
      "role": "errors",
      "text": "The `allow()` method may throw a `RedisException` if the Redis connection is lost. Wrap calls in try/catch blocks for production use:\n\n[code example]\n\nLettuce also supports automatic reconnection by default. If the connection to Redis is temporarily lost, Lettuce will attempt to reconnect and replay queued commands, providing better resilience than Jedis out of the box."
    },
    {
      "id": "learn-more",
      "title": "Learn more",
      "role": "related",
      "text": "* [EVAL command](https://redis.io/docs/latest/commands/eval) - Execute Lua scripts\n* [EVALSHA command](https://redis.io/docs/latest/commands/evalsha) - Execute cached Lua scripts\n* [Lua scripting](https://redis.io/docs/latest/develop/programmability/eval-intro) - Introduction to Redis Lua scripting\n* [HMGET command](https://redis.io/docs/latest/commands/hmget) - Get multiple hash fields\n* [HMSET command](https://redis.io/docs/latest/commands/hmset) - Set multiple hash fields\n* [Lettuce client](https://redis.io/docs/latest/develop/clients/lettuce) - Redis Lettuce client documentation"
    }
  ],
  "examples": [
    {
      "id": "the-lua-script-ex0",
      "language": "lua",
      "code": "local key = KEYS[1]\nlocal capacity = tonumber(ARGV[1])\nlocal refill_rate = tonumber(ARGV[2])\nlocal refill_interval = tonumber(ARGV[3])\nlocal now = tonumber(ARGV[4])\n\n-- Get current state or initialize\nlocal bucket = redis.call('HMGET', key, 'tokens', 'last_refill')\nlocal tokens = tonumber(bucket[1])\nlocal last_refill = tonumber(bucket[2])\n\n-- Initialize if this is the first request\nif tokens == nil then\n    tokens = capacity\n    last_refill = now\nend\n\n-- Calculate token refill\nlocal time_passed = now - last_refill\nlocal refills = math.floor(time_passed / refill_interval)\n\nif refills > 0 then\n    tokens = math.min(capacity, tokens + (refills * refill_rate))\n    last_refill = last_refill + (refills * refill_interval)\nend\n\n-- Try to consume a token\nlocal allowed = 0\nif tokens >= 1 then\n    tokens = tokens - 1\n    allowed = 1\nend\n\n-- Update state\nredis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)\n\n-- Return result: allowed (1 or 0) and remaining tokens\nreturn {allowed, tokens}",
      "section_id": "the-lua-script"
    },
    {
      "id": "installation-ex0",
      "language": "xml",
      "code": "<dependency>\n      <groupId>io.lettuce</groupId>\n      <artifactId>lettuce-core</artifactId>\n      <version>6.7.1.RELEASE</version>\n  </dependency>",
      "section_id": "installation"
    },
    {
      "id": "installation-ex1",
      "language": "groovy",
      "code": "implementation 'io.lettuce:lettuce-core:6.7.1.RELEASE'",
      "section_id": "installation"
    },
    {
      "id": "using-the-java-class-ex0",
      "language": "java",
      "code": "import io.lettuce.core.RedisClient;\nimport io.lettuce.core.api.StatefulRedisConnection;\n\npublic class Main {\n    public static void main(String[] args) {\n        // Create a Redis client and connection\n        RedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\n        StatefulRedisConnection<String, String> connection = redisClient.connect();\n\n        // Create a rate limiter: 10 requests per second\n        TokenBucket limiter = new TokenBucket(10, 1, 1.0, connection.sync());\n\n        // Check if a request should be allowed\n        TokenBucket.RateLimitResult result = limiter.allow(\"user:123\");\n\n        if (result.allowed()) {\n            System.out.printf(\"Request allowed. %.0f tokens remaining.%n\", result.remaining());\n            // Process the request\n        } else {\n            System.out.println(\"Request denied. Rate limit exceeded.\");\n            // Return 429 Too Many Requests\n        }\n\n        // Clean up\n        connection.close();\n        redisClient.shutdown();\n    }\n}",
      "section_id": "using-the-java-class"
    },
    {
      "id": "script-caching-with-evalsha-ex0",
      "language": "java",
      "code": "// The class handles script caching automatically.\n// First call loads the script, subsequent calls use EVALSHA.\nTokenBucket.RateLimitResult result1 = limiter.allow(\"user:123\"); // Uses EVAL + caches\nTokenBucket.RateLimitResult result2 = limiter.allow(\"user:123\"); // Uses EVALSHA (faster)",
      "section_id": "script-caching-with-evalsha"
    },
    {
      "id": "thread-safety-ex0",
      "language": "java",
      "code": "// Create shared resources\nRedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\nStatefulRedisConnection<String, String> connection = redisClient.connect();\nTokenBucket limiter = new TokenBucket(10, 1, 1.0, connection.sync());\n\n// Safe to call from multiple threads\nExecutorService executor = Executors.newFixedThreadPool(10);\nfor (int i = 0; i < 20; i++) {\n    final int id = i;\n    executor.submit(() -> {\n        TokenBucket.RateLimitResult result = limiter.allow(\"shared:resource\");\n        System.out.printf(\"thread %d: allowed=%b remaining=%.0f%n\",\n                id, result.allowed(), result.remaining());\n    });\n}\nexecutor.shutdown();\n\n// Clean up when done\nconnection.close();\nredisClient.shutdown();",
      "section_id": "thread-safety"
    },
    {
      "id": "asynchronous-usage-ex0",
      "language": "java",
      "code": "import io.lettuce.core.RedisClient;\nimport io.lettuce.core.api.StatefulRedisConnection;\n\nRedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\nStatefulRedisConnection<String, String> connection = redisClient.connect();\n\nAsyncTokenBucket limiter = new AsyncTokenBucket(10, 1, 1.0, connection.async());\n\nlimiter.allow(\"user:123\").thenAccept(result -> {\n    if (result.allowed()) {\n        System.out.printf(\"Request allowed. %.0f tokens remaining.%n\", result.remaining());\n    } else {\n        System.out.println(\"Request denied. Rate limit exceeded.\");\n    }\n}).join();\n\nconnection.close();\nredisClient.shutdown();",
      "section_id": "asynchronous-usage"
    },
    {
      "id": "reactive-usage-ex0",
      "language": "java",
      "code": "import io.lettuce.core.RedisClient;\nimport io.lettuce.core.api.StatefulRedisConnection;\n\nRedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\nStatefulRedisConnection<String, String> connection = redisClient.connect();\n\nReactiveTokenBucket limiter = new ReactiveTokenBucket(10, 1, 1.0, connection.reactive());\n\nlimiter.allow(\"user:123\")\n        .doOnNext(result -> {\n            if (result.allowed()) {\n                System.out.printf(\"Request allowed. %.0f tokens remaining.%n\", result.remaining());\n            } else {\n                System.out.println(\"Request denied. Rate limit exceeded.\");\n            }\n        })\n        .block();\n\nconnection.close();\nredisClient.shutdown();",
      "section_id": "reactive-usage"
    },
    {
      "id": "running-the-demo-ex0",
      "language": "bash",
      "code": "# Compile\njavac -cp lettuce-core-6.7.1.RELEASE.jar TokenBucket.java DemoServer.java\n\n# Run the demo server\njava -cp .:lettuce-core-6.7.1.RELEASE.jar DemoServer",
      "section_id": "running-the-demo"
    },
    {
      "id": "response-headers-ex0",
      "language": "java",
      "code": "int capacity = 10;\ndouble refillInterval = 1.0;\nRedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\nStatefulRedisConnection<String, String> connection = redisClient.connect();\nTokenBucket limiter = new TokenBucket(capacity, 1, refillInterval, connection.sync());\n\nTokenBucket.RateLimitResult result = limiter.allow(\"user:\" + userId);\n\n// Add standard rate limit headers\nresponse.setHeader(\"X-RateLimit-Limit\", String.valueOf(capacity));\nresponse.setHeader(\"X-RateLimit-Remaining\", String.valueOf((int) result.remaining()));\nresponse.setHeader(\"X-RateLimit-Reset\",\n        String.valueOf(System.currentTimeMillis() / 1000 + (long) refillInterval));\n\nif (!result.allowed()) {\n    response.setHeader(\"Retry-After\", String.valueOf((int) refillInterval));\n    response.setStatus(429); // Too Many Requests\n}",
      "section_id": "response-headers"
    },
    {
      "id": "using-as-a-servlet-filter-ex0",
      "language": "java",
      "code": "public class RateLimitFilter implements Filter {\n    private RedisClient redisClient;\n    private StatefulRedisConnection<String, String> connection;\n    private TokenBucket limiter;\n\n    @Override\n    public void init(FilterConfig config) {\n        redisClient = RedisClient.create(\"redis://localhost:6379\");\n        connection = redisClient.connect();\n        limiter = new TokenBucket(10, 1, 1.0, connection.sync());\n    }\n\n    @Override\n    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)\n            throws IOException, ServletException {\n        HttpServletRequest httpReq = (HttpServletRequest) req;\n        HttpServletResponse httpRes = (HttpServletResponse) res;\n\n        String key = \"ip:\" + httpReq.getRemoteAddr();\n        TokenBucket.RateLimitResult result = limiter.allow(key);\n\n        httpRes.setHeader(\"X-RateLimit-Remaining\",\n                String.valueOf((int) result.remaining()));\n\n        if (!result.allowed()) {\n            httpRes.setStatus(429);\n            httpRes.getWriter().write(\"{\\\"error\\\": \\\"Rate limit exceeded\\\"}\");\n            return;\n        }\n        chain.doFilter(req, res);\n    }\n\n    @Override\n    public void destroy() {\n        if (connection != null) connection.close();\n        if (redisClient != null) redisClient.shutdown();\n    }\n}",
      "section_id": "using-as-a-servlet-filter"
    },
    {
      "id": "error-handling-ex0",
      "language": "java",
      "code": "import io.lettuce.core.RedisException;\n\ntry {\n    TokenBucket.RateLimitResult result = limiter.allow(\"user:123\");\n    // Handle result\n} catch (RedisException e) {\n    System.err.println(\"Rate limiter error: \" + e.getMessage());\n    // Fail open: allow the request when Redis is unavailable\n    // Or fail closed: deny the request\n}",
      "section_id": "error-handling"
    }
  ]
}
