Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Created RedisRateLimiterNonAtomic #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
shameersheik wants to merge 3 commits into animir:master
base: master
Choose a base branch
Loading
from shameersheik:master

Conversation

@shameersheik
Copy link

@shameersheik shameersheik commented Sep 1, 2025

Issue : #222

  1. Tested with npm run eslint

Introduces non atomic redis rate limiter

it is designed to be non-atomic, meaning it does not guarantee that limit checking and update operations are performed atomically.
Advantages :

  1. It will be faster than RateLimiterRedis, as it does not wait till the Lua script is executed.
  2. Using lua scripts means that all operations are write operations that would go to a single Redis master write node, making read nodes useless.
    This implementation allows for read operations to be distributed across multiple Redis nodes, hence making it more scalable and faster.
    Disadvantages :
  3. This will be useful in scenarios where you want to allow concurrency and are okay with potential over-consumption of points till the read values are synced by redis.

}

// set up the initial state for update tries failures
this._updateTriesFailures = 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shameersheik _updateTriesFailures checks complicate the limiter.
I believe the limiter code shouldn't count failed tries on update.

Could you read a discussion in this PR #315 ?
There was a suggestion to add timeoutMs option inside the limiter.
It is similar to counting failed updates.
If you want to re-try update it can be configured on the level of Redis client.
Alternatively, a new wrapper can be developed. It would be much more flexible.

shameersheik reacted with eyes emoji
const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract");
const RateLimiterRes = require("./RateLimiterRes");

const incrTtlLuaScript = `redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shameersheik Could you explain how do you use this Lua script?
It doesn't look like non-atomic increment. It is the same atomic.

Copy link
Author

@shameersheik shameersheik Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the _upsert method , i donot wait for the response.

I get the keys using get method, calculate the response as if lua script is executed send it back
Trigger the lua script method through
upsertAndUpdateUpsertAttempts before but dont wait for it to get back

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this Lua script?
Non-atomic increment should be simple as this:

  1. get points
  2. newPoints = points + 1
  3. set new Points

Copy link
Author

@shameersheik shameersheik Sep 2, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole script is

"const incrTtlLuaScript = redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ local consumed = redis.call('incrby', KEYS[1], ARGV[1]) \ local ttl = redis.call('pttl', KEYS[1]) \ if ttl == -1 then \ redis.call('expire', KEYS[1], ARGV[2]) \ ttl = 1000 * ARGV[2] \ end \ return {consumed, ttl} \ ;"

other than just incrmenting new points, the script also checks and sets the ttl as well.

It is the same script that RateLImiterRedis.js uses.

Having this luascript will make the write operations (increment consumed and setting ttl) atomic.

In Summary :

  1. Get and Write calls are non atomic to each other because of this. Here upsert doesnot wait for write calls to finish.
  2. All the different writer operations (increment consumed and ttl setting) are atomic to each other because of the lua script.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shameersheik Thank you for explanation.
Non-atomic increments have advantages in comparison to atomic. First of all, they are faster. RateLimiterRedisNonAtomic should be truly non-atomic, otherwise there is no point in having a new rate limiter type. Developer can call get method from any limiter already.

How to implement non-atomic increments? When you get points from Redis, you can get ttl as well. Based on that ttl you can reset the value or increment it and set it back. You don't need anything except get, pttl and set methods to do non-atomic increments. You don't need Lua script. It should be simple and clear code.

The implementation of rate limiter in this PR is not ideal. It makes more get requests and then another atomic increment with Lua script. Could you rewrite it?

If you need to make configurable attempts you can create a wrapper for that. No need to complicate limiter code.

Copy link
Author

@shameersheik shameersheik Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original problem :
#222

It is not possible to horizontally scale due to using lua script's response as a read command in RedisRateLimiterRedis class.

Redis takes Lua script as a write command and hits the master node always not using the read nodes at all. This causes two problems.

  1. Increased latency - all commands go to a single node while other nodes can take up the work.
  2. Availability - if the master node is over utilized we cannot horizontally scale, only option would be to pay and increase the node memory. We cannot autoscale.

Problem in this thread :

Go one more step to split the ttl and the value increment commands as well to make them non atomic as well. To my knowledge, the redis keeps track of the increments(value) in an amount of time (ttl),

  1. if we split them further, what purpose would it solve ? , if i hit monitor log on the redis server, we will never even be able to figure out, which increments were even done in which timerange. - I would say this would cause confusion when debugging. From my experience, this was how I was able to figure out this issue Lua script takes non integer argument as input which causes random error on user supplied input #221
  2. We split the get value and set value, ttl commands from the lua script because it is advantageous to send get and set commands to different nodes (read and write). I donot see an use in splitting two set(write) commands since both of them would still go one by one (increasing network latency) to redis.

Do we want to rewrite this class name to something else other than saying it as non atomic. Let me know what you think.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shameersheik

if we split them further, what purpose would it solve

I don't suggest to split get and ttl commands. I suggest to use already implemented _get method, check it here. https://github.com/animir/node-rate-limiter-flexible/blob/master/lib/RateLimiterRedis.js#L152 It returns value and ttl in one multi call like this.

this.client
 .multi()
 .get(rlKey)
 .pttl(rlKey)

It perfectly works in Redis Cluster.
After you get the value you can increment it in Node.js and set a new value with set command. That's it.

I donot see an use in splitting two set(write) commands since both of them would still go one by one (increasing network latency) to redis

You don't need to do two set requests. One is enough.

Copy link
Author

@shameersheik shameersheik Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So instead of calling the lua script after get here, we call the put command directly ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly.
non-atomic upsert should be simple as: get value, newValue = value + 1, set newValue.

shameersheik reacted with thumbs up emoji
@animir animir marked this pull request as draft September 5, 2025 09:49
Copy link
Owner

animir commented Sep 23, 2025

@shameersheik Hey, are you going to continue implementation of the non atomic limiter?

Copy link
Author

@shameersheik Hey, are you going to continue implementation of the non atomic limiter?

Yeah, will let you know if I have any new updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@animir animir animir left review comments

+1 more reviewer

@atetta atetta atetta left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /