Conversation
NewUnlimited fakes unlimited access with a MaxUint32 bucket refilled
every millisecond. Each instance starts a ticker and worker that wake
even when nobody is calling Take, and Take itself still pays for a
channel hop.
Make unlimited mode explicit instead. Take returns immediately and
CanTake reports true without a ticker, token channel, or background
goroutine. That also drops the extra cost for unlimited keys in
MultiLimiter and AutoLimiter.
Keep runtime config working by spinning up a finite bucket on the first
SetLimit or SetDuration call. Keep the initial MaxUint32 burst, apply
limit changes at the next refill, and leave the default interval at
1 ms. The refill clock now starts at the first setter, not at
construction.
Serialize bucket init, config, and Stop. Drop the mutex before waiting
for a token, and don't let setters create a bucket after Stop. Keep the
original GetLimit value and reject non-positive refill durations.
PD consumers this hits:
- subfinder, urlfinder, and tldfinder: create an unlimited bucket for
each selected source that has no positive source-specific or global
rate limit, so idle workers pile up on a normal run.
- nuclei: uses an unlimited global limiter when global limiting is off
or per-host limiting is on, plus extra SDK paths.
- httpx: picks unlimited mode when neither the per-second nor
per-minute rate is positive.
- dnsx: creates an unlimited limiter before checking the configured
rate. A positive rate replaces it without stopping the old worker, so
the idle work still happens even with a finite limit.
- uncover: exposes unlimited mode through sources.NewSession when
rateLimit <= 0. The normal service path already falls back to a
finite 30/min for a zero rate.
- [and some internal projects that can't be named here]
Those call sites pick up the win after they adopt a release with this
change. This is about exposure, not measured app-level CPU savings.
Ten-run benchmark medians on Linux/amd64 with Go 1.26.5 and
GOMAXPROCS=4:
before after
Take 835 ns/op 23.3 ns/op
Parallel Take 1334 ns/op 60.5 ns/op
MultiLimiter Take 631 ns/op 40.0 ns/op
Idle CPU, 30 limiters 127 ms/s 2.03 ms/s
Idle CPU, 1000 limiters 1473 ms/s 2.11 ms/s
Idle workers, 1000 1000 0
Idle CPU after the change is basically the zero-limiter control
(1.92 ms/s). These numbers are unlimited mode before any setter calls;
configured limiters still use the finite bucket.
Signed-off-by: Dwi Siswanto <git@dw1.io>
Member
Author
|
@coderabbitai create an issue to track this, make it short, one paragraph. must be bug oriented. |
Member
Author
|
Measured through Subfinder using pinned ratelimit@v0.0.88 vs. ratelimit@416c32d8:
Six interleaved runs, with a separate dependency control. |
Mzack9999
approved these changes
Sep 9, 2026
dwisiswant0
deleted the
dwisiswant0/perf/avoid-timer-worker-overhead-in-unlimited-mode
branch
September 9, 2026 22:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NewUnlimited fakes unlimited access with a MaxUint32 bucket refilled
every millisecond. Each instance starts a ticker and worker that wake
even when nobody is calling Take, and Take itself still pays for a
channel hop.
Make unlimited mode explicit instead. Take returns immediately and
CanTake reports true without a ticker, token channel, or background
goroutine. That also drops the extra cost for unlimited keys in
MultiLimiter and AutoLimiter.
Keep runtime config working by spinning up a finite bucket on the first
SetLimit or SetDuration call. Keep the initial MaxUint32 burst, apply
limit changes at the next refill, and leave the default interval at
1 ms. The refill clock now starts at the first setter, not at
construction.
Serialize bucket init, config, and Stop. Drop the mutex before waiting
for a token, and don't let setters create a bucket after Stop. Keep the
original GetLimit value and reject non-positive refill durations.
PD consumers this hits:
each selected source that has no positive source-specific or global
rate limit, so idle workers pile up on a normal run.
or per-host limiting is on, plus extra SDK paths.
per-minute rate is positive.
rate. A positive rate replaces it without stopping the old worker, so
the idle work still happens even with a finite limit.
rateLimit <= 0. The normal service path already falls back to a
finite 30/min for a zero rate.
Those call sites pick up the win after they adopt a release with this
change. This is about exposure, not measured app-level CPU savings.
Ten-run benchmark medians on Linux/amd64 with Go 1.26.5 and
GOMAXPROCS=4:
Idle CPU after the change is basically the zero-limiter control
(1.92 ms/s). These numbers are unlimited mode before any setter calls;
configured limiters still use the finite bucket.