A public "check this code" endpoint is catnip for brute-force enumeration, but a blunt rate limit locks out the legitimate user who fat-fingered one character. The trick is limiting the right dimension.
Limit by outcome, not just by IP
Count failed validations per identity, and let successes flow freely. A user applying one valid code should never hit a wall; a script guessing thousands should hit it fast:
if not code_is_valid(code):
fails = redis.incr(f"promofail:{ip}:{hour}")
redis.expire(f"promofail:{ip}:{hour}", 3600)
if fails > 20:
raise TooManyRequests()
Token bucket beats fixed windows
Fixed windows allow a burst at the boundary — 20 requests at 10:59:59 and 20 more at 11:00:00. A token bucket smooths that into a steady refill rate and a small burst allowance, which matches how humans actually type.
Make the error honest but quiet
Return the same generic "invalid or expired" for unknown and expired codes so enumeration learns nothing from the response, but keep the HTTP status distinct from your rate-limit 429 so clients can back off correctly.
Reference
Bookmakers publish time-boxed bonus codes that are a natural target for enumeration, so how they gate validation is instructive. A page like Mostbet Kod Promocyjny shows codes tied to explicit expiry and eligibility rules — exactly the metadata your validator should check before it ever touches the rate limiter.
Takeaway
Rate-limit failures per identity with a token bucket, keep error messages uninformative to scanners, and never make a valid single redemption feel like an attack. Security that punishes real users is just a different outage.
Top comments (0)