Quick fix
# One-liner: IP rate limiting at the nginx layer (zone in http block, enable inside location; over-limit returns 503)
# http block: limit_req_zone $binary_remote_addr zone=sms:10m rate=5r/s;
# inside location: limit_req zone=sms burst=10 nodelay;
burst = burst bucket capacity; nodelay = reject immediately over limit; default rejection is 503, limit_req_status 429 switches to 429.
What you will run into
The SMS endpoint gets hammered by site docs traffic: huge request volumes in a short window, SMS costs spiking, the endpoint overwhelmed.
Generalizing to any business endpoint: same IP/same phone number high-frequency requests, proxy IP pool rotation, verification-code endpoints being credential-stuffed.
One abuse wave is real money lost.
Why this happens
The endpoint has no rate limiting and no blacklist: an SMS/verification-code endpoint can naturally be called infinitely (no frequency, no origin restriction).
Emergency stop-loss measures (circuit breaker / degradation / blacklist / rate limit) are missing or ineffective config — measured finding: limit_req and return in the same location short-circuit, config "looks present" but isn't.
| Protection layer | Means | Measured status |
|---|---|---|
| Emergency stop-loss | Test mode / circuit breaker, Nginx deny, global rate limit | ✅ nginx deny measured 403 |
| Nginx rate limit | limit_req_zone + limit_req | ✅ Works measured in static/proxy forms (503) |
| Business layer | Redis frequency, Bloom filter, device fingerprint | ⏳ No redis/docker on this machine; not measured |
| Monitoring review | Request-volume baseline + anomaly alerts | Long-term after go-live |
SMS/verification APIs get abused because they are "stateless endpoints without rate constraints": the endpoint does not track call sources or counts, so attackers can call it indefinitely (credential stuffing, harassment, bill-shaving) while you pay the cost.
The "config ineffective" trap is separate: when nginx limit_req and return live in the same location, return short-circuits subsequent directives — the config "looks present" but is not, a trap reproduced in testing.
Deterministic takeaway: verify protections actually take effect first (tested: nginx deny returns 403, limit_req returns 503) — an unverified config equals no config;
protections come in three layers — network (nginx deny/rate-limit, deployable immediately), business (Redis counters/device fingerprinting, needs dependencies), monitoring (baseline + alerts, long-term);
for emergency stop, test-mode/circuit-breaking stops the bleed first, then harden; fixed diagnostic: when abused, first confirm whether the protection really returns the expected status code, then discuss strategy.
Step by step
1. Emergency stop-loss (golden 30 minutes) — Required
In the config center, switch the SMS endpoint to "test mode" (returns success but doesn't actually send) or circuit-break it; block abnormal IPs at the nginx layer (deny, measured 403); if ineffective, temporarily disable SMS to protect core business.
2. Nginx rate limiting (measured working form) — Required
Define the zone in the http block:
limit_req_zone $binary_remote_addr zone=sms:10m rate=5r/s;
Enable inside the location (works in both proxy and static-file forms, measured):
limit_req zone=sms burst=10 nodelay;
3. Business-layer protection — Required
Phone-number frequency limits (Redis INCR+EXPIRE, auto-reset on expiry); Bloom filter to fast-filter invalid/risky numbers (12MB for tens of millions); device fingerprint / behavior risk control; API signing.
(No redis-server or docker daemon on this machine; not measured, stated honestly.)
4. Monitoring review — Required
Build request-volume baselines by IP/phone/device with automatic anomaly alerts; gray-release new risk rules.
How to confirm it works
Load-test assertions (measured output shape: 15 rapid-fire requests under rate limit):
curl 15 rapid requests
→ 1×200 + 14×503 (rate=2r/s, static-file form)
→ error log: limiting requests, excess: 0.974 by zone "min"
After deny 127.0.0.1, requests → 403 (log: access forbidden by rule).
Other environments
| Environment | Notes |
|---|---|
| Static-file form | limit_req measured working: 1×200 + 14×503 |
| proxy_pass proxy form | Production form measured working: 1×200 + 14×503 |
| deny + return same location | Measured ineffective (return short-circuits the preaccess/access stages) |
| limit_req + return same location | Measured ineffective (all 200, no log entries) — a pitfall the article didn't mention |
| Business layer | Redis token bucket / Bloom filter not measured (no redis-server / docker daemon) |
Symptom
An SMS endpoint gets hammered by site docs traffic: a huge number of requests in a short window, SMS costs spike, the API is overwhelmed; generalized to any business endpoint — same IP/same phone number high-frequency requests, rotating proxy IP pools, verification-code endpoints brute-forced.
Cause
The endpoint has no frequency limits or blacklists: SMS/verification endpoints are naturally callable without limit (no frequency, no source restrictions); once hammered it is real money lost; emergency stop mechanisms (circuit breaker / degrade / blacklist / rate limit) are missing or misconfigured (see evidence: limit_req + return in the same location gets short-circuited — config "looks on" but is actually off).
Solution
- Emergency stop (golden 30 minutes): switch the SMS endpoint to "test mode" (returns success but does not really send) or circuit-break it in the config center; block offending IPs at nginx (
deny, measured 403); enable global rate limiting at the gateway; if ineffective, temporarily disable the SMS feature to protect core business. - nginx rate limiting (measured working form):
limit_req_zone $binary_remote_addr zone=sms:10m rate=5r/s; # http block
# inside location (both reverse-proxy and static-file forms measured working, excess → 503):
limit_req zone=sms burst=10 nodelay;
burst= burst bucket capacity,nodelay= reject excess immediately; default excess status is 503 (change withlimit_req_status 429).- Measured pitfall (not in the article):
limit_req/denytogether withreturnin the same location do NOT work —returnterminates the request in the rewrite phase, skipping the preaccess/access phases;location = /api/sms/send { limit_req ...; return 200 "ok"; }is effectively unlimited. The endpoint must use a static-file/proxy_passform, or put the limit at theserverlevel.
- Application-layer protection: per-phone-number rate limits (Redis
INCR+EXPIRE, auto-reset on expiry); a Bloom filter to quickly drop invalid/risky numbers (12MB for tens of millions); device fingerprinting / behavior risk control; API signatures. No redis-server / docker daemon locally, so not measured (stated honestly). - Monitor & review: per-IP/number/device request baselines, auto-alert on anomalies; gray-release new risk rules.
Verification Evidence
- Local test (2026-08-29, nginx/1.28.3 nginx-light + python3, fully local zero network):
- Control: 15 consecutive requests to an unlimited endpoint — all 200.
limit_req zone=min rate=2r/s+ static file: 15 consecutive = 1×200 + 14×503, error loglimiting requests, excess: 0.974 by zone "min".limit_req rate=2r/s+proxy_passreverse proxy: same as 2 (production form works).deny 127.0.0.1: 403, logaccess forbidden by rule.- Pitfall:
limit_req + return 200in the same location — 15 consecutive all 200, no log entries;deny + returnalso ineffective —returnshort-circuits the preaccess/access phases (found by testing the first broken config; results 2/3/4 came after fixing it).
- Redis/Lua token bucket, Bloom filter: not measured (no redis-server locally; docker daemon not running, image pulls restricted).
Evidence details kept locally (2026-08-29)
Applicable tools (1)
This problem is linked from the tool pages below — reachable from either side.
nginxSource & evidence
← Security & Governance domain | Problem Catalog Experience | Facing another issue? Go to the Helpdesk