Marcio Cunha

How to protect APIs against brute force using Redis and Lua

Learn how to implement an atomic and distributed rate limiter in your APIs using Lua scripts in Redis to effectively block brute force attacks.

Marcio Cunha•3 min
Also available in:PortuguêsEspañol
Summary
  • Lua scripts execute atomically in Redis, preventing race conditions in high-concurrency environments.
  • Decentralized request control protects distributed services without overloading relational databases.
  • Precise counting by IP or token blocks automated password guessing attempts on critical endpoints.
  • Clear separation between expiration and increment logic ensures low memory consumption in cache.
  • High-performance traits make this approach ideal for microservices under intense traffic.

The challenge of controlling API access under attack

When you deploy an API on the internet, the biggest nightmare is not just high legitimate traffic, but malicious password-guessing attempts. Brute force attacks overwhelm servers and consume precious resources trying to crack credentials. In practice, this means we need a gateway barrier that can count requests per user or IP in real time. If we leave this counting to traditional databases, the slowdown generated by frequent queries can crash the very system we are trying to protect.

Why naive solutions fail in distributed environments

Many developers try to solve access control by storing attempt counts in application memory variables or simple database tables. The problem arises when we scale the API to multiple servers running in parallel behind a load balancer. Each machine only sees a fraction of the accesses, allowing an attacker to multiply real attempts without exceeding individual limits. To solve this, we need a high-speed centralized repository, which is precisely where learning how to implement asynchronous CQRS in high traffic APIs comes into play for handling massive data flows.

The role of Redis and the danger of race conditions

Redis is an extremely fast in-memory database famous for storing key-value data structures. In common scenarios, we check a counter with a GET command, add a value in the application code, and save it back with SET. However, in highly concurrent environments, two parallel requests can read the same initial value simultaneously, resulting in an incorrect count. This phenomenon is known as a race condition. In practice, the system lets more requests through than it should because reading and writing did not happen in a single indivisible step.

Ensuring atomicity with Lua scripts in Redis

To eliminate synchronization flaws, Redis allows executing scripts written in the Lua language directly inside the database server itself. When we send a Lua script to Redis, it executes the entire block of code strictly atomically. No other command can interfere or alter the data while the script is running. This means we can check the current limit, add a new attempt, and set the expiration time in microseconds, ensuring absolute precision even when hundreds of users try to log in simultaneously.

Implementing the rate limiter in practice

Below is a classic example of a Lua script that implements a sliding window or expiration-based rate limiter. The code checks if the key exists, increments the value, and sets a retention time if it is the first attempt of that period. To better understand how to structure robust applications handling this type of logic and external dependencies, it is worth checking out how to apply Clean Architecture in high performance Go APIs to keep your code clean and testable.

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])

local current = redis.call('get', key)
if current and tonumber(current) >= limit then
    return 0
end

current = redis.call('incr', key)
if tonumber(current) == 1 then
    redis.call('expire', key, expire_time)
end

return current

Handling failures and maintaining system resilience

Although Redis is extremely stable, networks can fail and servers can restart unexpectedly. If the cache goes down, your API should not completely stop working just because the limiter became unavailable. In practice, we apply a fail-open or fail-closed concept depending on the criticality of the login endpoint. In high-security scenarios, we can choose to temporarily block or record a critical alert, similar to the principles we use when protecting microservices from cascading failures with circuit breakers to prevent a single component from breaking the entire application.

Final considerations on security and performance

Protecting your authentication routes against automated attacks is an indispensable pillar to maintain the integrity of any modern system. The combined use of Redis with Lua scripts solves the classic problem of distributed concurrency without sacrificing the response speed users expect. By centralizing this logic in the cache, you save vital resources of the primary database and ensure solid defense against persistent attackers.