Consumer Wallet Rate-Limit Race-Condition Automation: A Limit That Two Requests Could Both Ignore at Once

A rate limit that checks-then-writes without a lock is not actually a limit under concurrency
A rate limiter built as "count how many attempts happened recently, and reject this one if that count is already too high" is intuitive and, under ordinary, one-at-a-time traffic, works exactly as intended. The design has a specific, easy-to-miss weakness the moment two attempts can arrive close enough together to both execute their own "count, then decide" sequence before either one's own attempt is written down: both requests see the same, not-yet-updated count, both conclude independently that the limit has not been reached, and both proceed — even in cases where the combined total, had it been checked as a single, indivisible operation, should have blocked the second one outright. The limiter is not wrong in its arithmetic; it is wrong in its timing.
How the underlying problem shows up before you fix it
A rate limiter is implemented as two separate steps — read the current count, then decide whether to allow the new attempt — with no mechanism preventing a second, concurrent request from performing its own read before the first request's own attempt has been recorded.
The count is computed by querying a general-purpose table (here, an audit log) that was not specifically designed, or indexed, or locked, for the purpose of enforcing a hard concurrency limit.
The limiter behaves correctly under ordinary, sequential load and only fails under genuine concurrency — meaning routine manual testing, which rarely fires multiple simultaneous requests deliberately, can pass cleanly while the underlying race remains completely unaddressed.
The number of requests that can slip through during the race scales with how many arrive within the same narrow timing window, rather than being capped at a small, fixed number — a burst of concurrent requests, not just two, can all pass the same stale check.
Proving the defect (and later, the fix) requires deliberately generating genuine concurrent load against the limiter — a sequential test, no matter how many requests it sends, will never reproduce a race that depends on requests overlapping in time.
How a count-then-decide limiter became a single, indivisible operation
Centriu Loop's consumer portal enforces a rate limit on how many times a single IP address may attempt to open a session within a given window, specifically to blunt automated probing. The previous implementation determined whether an address had exceeded its limit by counting matching rows in an audit-log table — a table that records sensitive operations across the product generally, not a structure purpose-built to enforce a hard concurrency guarantee.
That counting step and the decision that followed it were not wrapped in anything that would prevent a second request from reading the count before the first request's own attempt had been written down. Under ordinary, one-request-at-a-time traffic, this timing gap is invisible: each request's count reflects every attempt that came before it, because there is no overlap for the gap to matter within. Under genuine concurrency — multiple requests from the same address arriving close enough together — the gap becomes exploitable structurally, not through any cleverness on an attacker's part: request A reads the count, request B reads the same count before A's own attempt is recorded, and both independently and correctly conclude, based on the information each one had, that the limit had not yet been reached. Both proceed. The limiter's own arithmetic was never wrong; the sequence around it simply allowed multiple requests to act on the same, momentarily stale information.
The fix does not try to patch the existing check-then-write sequence with a faster count or a shorter window — both would narrow the race without closing it. Instead, the counting mechanism moved into the database itself, using a dedicated table built specifically for this purpose rather than the general-purpose audit log, and the count-and-record sequence for a given IP address is now serialized using a database-level advisory lock scoped specifically to that address's own hashed identifier, held for the duration of a single transaction. Any second request from the same address arriving while the first is still being processed must wait for that lock to release before it can perform its own count — at which point the first request's own attempt has already been recorded, and the second request sees a count that correctly includes it.
The fix was verified specifically under the condition that actually exposes the original defect: not a sequential series of requests, however many, but sixteen requests fired at the same address at the same time, confirming the limit held under genuine, deliberate concurrency rather than merely under the sequential traffic pattern that had made the original race invisible to ordinary testing.
What is actually built today
The consumer-portal rate limiter's count for a given IP address is computed from a dedicated table built specifically for this purpose, rather than from a general-purpose audit log not designed to enforce a hard concurrency guarantee.
The count-and-record sequence for a given IP address is serialized inside a single database transaction using an advisory lock scoped to that address's own hashed identifier — a second concurrent request from the same address must wait for the first to finish being recorded before it can read the count.
The fix is verified specifically under genuine concurrency: 16 simultaneous requests from one address, confirming the limit holds when requests actually overlap in time, not merely when they arrive one after another.
The IP address itself is stored only as a hash within the dedicated rate-limiting table, not in plaintext — the table exists specifically to enforce the limit across every organization an address might be probing, which requires recognizing the SAME address consistently, but does not require storing it unhashed to do so.
The fix required no change to the rate limit's own thresholds or windows — it corrects the mechanism that ENFORCES whatever threshold is configured, independent of what that threshold's specific numeric value is.
Sixteen requests, one address, one moment (illustrative framing of the actual fix)
Before the fix, an automated tool sending a burst of simultaneous requests from a single IP address could have several of them — not just the first — all read the rate limiter's count before any of their own attempts were recorded, and all proceed past a limit that, checked correctly, should have stopped every request beyond the first few. After the fix, the identical burst is serialized at the database level: each request's count-and-record sequence completes fully before the next one can begin its own check, so the limit holds exactly as configured regardless of how many requests arrive at the same instant.
What changes operationally
Centriu Loop's consumer-portal rate limiter now enforces its configured limit correctly under genuine concurrent load, closing a race condition that previously allowed multiple simultaneous requests from the same address to each read a stale, not-yet-updated count and proceed together — verified specifically by firing 16 simultaneous requests and confirming the limit held.
When this is not the right fit
This automation governs the internal concurrency-safety of Centriu Loop's own consumer-portal rate limiter — it does not change the limiter's own configured thresholds or windows, does not add a customer-facing setting, and does not affect rate limiting on any other Centriu Loop endpoint or any other Centriu product, which this fix does not touch.
A count-then-decide check vs. a single, database-enforced atomic operation
Implementing a rate limit as "count recent attempts, then decide" in application code is straightforward to write and reason about under ordinary, sequential traffic — the risk is specific to genuine concurrency, which sequential manual testing rarely exercises and therefore rarely catches. Moving the count-and-decide sequence into the database itself, serialized with a lock scoped to exactly the resource being limited (here, one IP address), removes the timing gap entirely by making the check-and-record a single, indivisible operation — at the cost of needing to deliberately test under real concurrent load, since a sequential test cannot distinguish a correctly atomic limiter from one that merely got lucky.
Related systems
Main system: Centriu Loop.
What it does NOT do
- Does not change Centriu Loop's own configured rate-limit thresholds or time windows — this fix corrects the mechanism that enforces whatever threshold is configured, not the threshold's own numeric value.
- Does not affect rate limiting on any other Centriu Loop endpoint, or on any other Centriu product — this fix is specific to the consumer-portal session-opening rate limiter.
- Does not retroactively identify whether this specific race condition was ever actually exploited before the fix shipped — a team with that concern would need its own historical request-log analysis at the timing granularity required, which this fix does not provide.
- Does not store IP addresses in plaintext in the dedicated rate-limiting table — addresses are hashed, consistent with treating them as data to recognize repetition in, not data to retain in readable form.
- Does not overlap with the identifier-based lookup elimination fix or the organization-slug enumeration fix — both are separate, independent mechanisms from the same source commit, covered on their own pages.
Security and governance
Centriu Loop's consumer-portal rate limiter enforces its configured attempt limit atomically, using a database-level advisory lock scoped per IP address inside a single transaction — closing a race condition that previously let multiple simultaneous requests from the same address each act on a stale count. Verified under genuine concurrent load (16 simultaneous requests). Any personal data referenced remains subject to Brazil's LGPD (Law No. 13,709/2018). Full detail on access control lives at /governanca and /iso.
Pricing and contracting
Available by monthly subscription, with tiered plans. Values and terms come from the official pricing table at /precos (Centriu's central source — never restated here).
Frequently asked questions
Was this race condition actually observed, or is it theoretical?
The fix was verified specifically by generating genuine concurrent load — 16 simultaneous requests from one address — and confirming the previous, unlocked counting approach was the class of defect that would allow such a burst through; the replacement was proven to hold under the identical test.
Why didn't ordinary testing catch this earlier?
A rate limiter built as count-then-decide behaves correctly under ordinary, one-request-at-a-time traffic, which is what most manual and even most automated testing naturally produces. The defect only appears when multiple requests genuinely overlap in time, which requires deliberately generating concurrent load to reproduce.
Did the rate limit's actual numeric thresholds change?
No — this fix corrects the mechanism that enforces whatever threshold is configured. The thresholds themselves are unchanged by this fix.
Are IP addresses stored in plaintext to make this fix work?
No — the dedicated rate-limiting table stores only a hash of the IP address, sufficient to recognize the same address making repeated attempts without retaining it in readable form.
Does this fix affect rate limiting elsewhere in Centriu Loop or other Centriu products?
No — it is specific to the consumer-portal session-opening rate limiter described here.
What does Centriu Loop cost?
It is sold by subscription with a published starting price — exact current values are on the central pricing page.
See how Centriu Loop enforces rate limits under real concurrent load
Reach our commercial team directly, or leave your details below — we'll follow up with guidance for your case.
