Repository navigation
getTokensRemaining() returns unexpected values #61
Description
I've been using this package for years to provide rate limiting for a chat application and it has worked really well for me. I use a limiter for message rate (flood protection) and when the limiter has <= 50% tokens remaining, the server should send a warning message to the user.
Some users have always been able to trigger flood protection w/o getting any warning from the server and I have finally figured out why. Please consider the following code:
const RateLimiter = require('limiter').RateLimiter
const limiter = new RateLimiter(10, 'minute')
const remove = () => {
let date = new Date()
let remaining
if (limiter.tryRemoveTokens(1)) {
remaining = limiter.getTokensRemaining()
console.log(`${date} - token removed, remaining: ${remaining}`)
} else {
console.log(`${date} - NO tokens remaining`)
}
}
remove()
setInterval(() => {
remove()
}, 3500)Note the interval of 3.5 seconds.
This is the output:
Sat May 04 2019 17:05:03 GMT-0400 (EDT) - token removed, remaining: 9
Sat May 04 2019 17:05:07 GMT-0400 (EDT) - token removed, remaining: 8.584
Sat May 04 2019 17:05:10 GMT-0400 (EDT) - token removed, remaining: 8.168
Sat May 04 2019 17:05:14 GMT-0400 (EDT) - token removed, remaining: 7.751999999999999
Sat May 04 2019 17:05:17 GMT-0400 (EDT) - token removed, remaining: 7.336166666666665
Sat May 04 2019 17:05:21 GMT-0400 (EDT) - token removed, remaining: 6.920166666666665
Sat May 04 2019 17:05:24 GMT-0400 (EDT) - token removed, remaining: 6.504166666666665
Sat May 04 2019 17:05:28 GMT-0400 (EDT) - token removed, remaining: 6.088166666666664
Sat May 04 2019 17:05:31 GMT-0400 (EDT) - token removed, remaining: 5.672166666666664
Sat May 04 2019 17:05:35 GMT-0400 (EDT) - token removed, remaining: 5.255999999999998
Sat May 04 2019 17:05:38 GMT-0400 (EDT) - NO tokens remaining
[SNIP]
Sat May 04 2019 17:06:06 GMT-0400 (EDT) - token removed, remaining: 9
You can see here that getTokensRemaining() never returns a value below 50% of the bucket size at this interval. My expectation is that remaining tokens would decrement 10, 9, 8, 7 etc. so I'm really confused why this is happening. Even though 10 tokens were removed, getTokensRemaining() reported 5.255999999999998 when it should have reported 0 (at least in my mind).
Plus I might be confused as to how this works, but I would expect tokens to be slowly added back to the bucket over time. What appears to be happening here is that about a minute after the first removal, the bucket is filled back up with 10 tokens.
I've been digging through tickets and the code but I can't quite figure out what's happening here and what I can do about it. This seems like it might be a bug so I thought I'd report it.
Activity
[-]Oddities with getTokensRemaining()[/-][+]getTokensRemaining() returns unexpected values[/+]brenc commentedon May 4, 2019
Looks like using
TokenBucketitself gets the results I expect:Output:
Tokens appear to be getting dripped back into the bucket over the course of the interval vs. being dumped into the bucket all at once at the beginning of the next interval (as when using
RateLimiter).texpatnyc commentedon Aug 4, 2021
@brenc Were you ever able to resolve this issue? I am having the same issue and finding the package unusable because of it.
brenc commentedon Aug 4, 2021
I ended up creating my own
TokenBucketclass, something like this:Here's how it's used:
This has worked well for several years. There are probably better solutions esp. if you need to scale beyond one process.
jhurliman commentedon Sep 10, 2026
The distinction identified here is now documented in 4.0.0:
getTokensRemaining()reports the continuously refilled token bucket, whileRateLimiteralso has a separate per-interval allowance. A positive bucket balance therefore does not imply that another request is allowed in the current interval.tryRemoveTokens(count)checks and consumes against both constraints.Closing this API-behavior question as documented, without changing the meaning of the existing balance method. A follow-up PR adds
RateLimiter.getWaitTime(count)so callers can display a cooldown without consuming tokens (#88). A standaloneTokenBucketremains appropriate when continuous refill without the additional interval counter is the intended policy.