• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

Fixing Race Conditions in Redis Counters: Why Lua Scripting Is the Key to Atomicity and Reliability

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
1,481
Баллы
155
If you've ever built rate limiting or login throttling with Redis, chances are you've used INCR and EXPIRE. Seems easy, right?

But if you're doing them in two separate steps, you might be in for a surprise—especially under high load.

Let’s talk about the subtle (and annoying) race condition this causes, why plain Redis transactions don’t fix it, and how Lua scripting totally saves the day.

The Situation: Tracking Failures Over Time


Let’s say you’re trying to block brute-force login attempts. So every time a login fails, you increment a counter:


const count = unwrap(await redis.fire("incr", failureKey)) as number;
if (count === 1) {
await redis.fire("expire", failureKey, WINDOW_SEC);
}

What’s going on here:

  • failureKey might be something like login:fail:user123
  • WINDOW_SEC is how long the counter should live (like 60 seconds)
  • If it’s the first failure, you kick off the timer by setting a TTL

It works… mostly.

The Sneaky Race Condition ?


This setup breaks under concurrency. Here’s how:


  1. Client A does INCR, gets 1


  2. Client B also does INCR, gets 2


  3. Now Client A sets the TTL

Oops. Now the window starts after 2 failures, not 1. You’ve just let in an extra try, and maybe more depending on timing.

That’s a classic race condition—and these are exactly the kind of bugs you don’t want to debug in production at 2AM.

Can’t We Just Use Transactions? ?


That’s a good thought—and Redis does support transactions via MULTI and EXEC. But here’s the catch:


await redis.multi()
.incr(failureKey)
.expire(failureKey, WINDOW_SEC)
.exec();

This groups the commands together, but not conditionally. Redis will always run both commands—INCR and EXPIRE—no matter what. So you can’t say “expire only if the count is 1.”

And while Redis MULTI ensures the commands run one after another, they still aren’t atomic in the sense that no logic runs between them. Another client can sneak in between your INCR and EXPIRE, and mess things up.

So yeah—transactions alone don’t solve this. You need logic + atomic execution.

Lua Scripting Saves the Day ✨


Redis has a built-in feature for this: Lua scripts.

They let you run multiple commands on the Redis server as a single, atomic operation. Here’s the script:


local count = redis.call("INCR", KEYS[1])
if count == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return count

What’s happening:

  • We increment the key
  • If it’s the first increment, we set the expiration
  • Then return the new count

All of this happens as one atomic command. No other Redis client can sneak in during this execution. Problem solved.

Why Lua Rocks for This

  • No more race conditions ✅
  • Fewer round-trips to Redis
  • Cleaner logic—you move complexity server-side

Basically, it's safer and faster.

How to Use It in Node.js


If you're using something like ioredis, here’s how you'd fire off that Lua script:


const luaScript = `
local count = redis.call("INCR", KEYS[1])
if count == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return count
`;

const count = unwrap(
await redis.fire("eval", luaScript, 1, failureKey, WINDOW_SEC),
) as number;

  • eval tells Redis to run the script


  • 1 is the number of keys we’re passing in


  • failureKey goes into KEYS[1], WINDOW_SEC into ARGV[1]

Any time you're chaining Redis commands where order and timing matter, and especially when things can get concurrent—Lua scripting is your go-to

It makes your logic bulletproof, saves you network hops, and helps you sleep at night knowing your counters won't glitch out under pressure.

Hey! I recently created a tool called

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.

Feel free to check it out, and if you like it, consider leaving a generous star on my GitHub! ?

Let's connect!!: ?


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу