Nginx 429 Dropping Orders: Whitelist Payment Webhooks
Cash cleared but orders vanished? Payment webhooks were hitting Nginx HTTP 429. This guide shows log checks, a native geo whitelist for provider IPs, reload steps, and when Australian stores should book Fixwebnode.
If your Australian store is taking payments but order records never land, rate-limited payment webhooks are a prime suspect—and you can confirm it in the Nginx access log before you touch the cart code.
This runbook walks through diagnosing HTTP 429 blocks on provider callbacks, building a native Nginx geo map whitelist for the payment provider’s IP ranges, reloading the stack safely, and verifying that order data sticks. Fixwebnode works remote-first with Australian sole traders and local operators on exactly this class of e-commerce breakage—start at website repair Australia if you want a specialist on the call while you keep the till open.
Why cash without orders is an Nginx problem, not “just WooCommerce”
Payment providers (Stripe, PayPal, Afterpay-style gateways, bank HPP callbacks) post server-to-server webhooks after capture. Your storefront may show a success page because the browser redirect succeeded, while the webhook that creates the order, stock decrement, and fulfilment row is rejected upstream. When Nginx limit_req or a shared anti-abuse zone returns 429 Too Many Requests, the provider retries a few times, then gives up. Money is captured; your database never sees the event.
In Australia this shows up hardest on flash sales, end-of-financial-year campaigns, and multi-store headless setups where many callbacks share one public IP edge. The fix is not “turn off rate limiting.” The fix is to keep limits for browsers and bots while exempting the provider’s published IP ranges with Nginx’s built-in geo module, then reload without dropping legitimate traffic.
Why are payment webhooks returning HTTP 429 and dropping Australian store orders?
Payment webhooks hit HTTP 429 when they share a global or per-IP limit_req zone with shoppers and scrapers. Nginx answers 429 before PHP or Node ever runs, so the gateway charges the card but your order tables stay empty. Whitelist the provider’s IP ranges with a native geo map, skip the limit key for those addresses, test-reload Nginx, and confirm 200s on the webhook path in the access log.
| Symptom | Quick fix | Call Fixwebnode when |
|---|---|---|
Access log shows 429 on /webhooks/* or /api/payment/* | Confirm source IPs match the provider; draft a geo whitelist | You run multiple vhosts or a WAF in front and cannot map zones cleanly |
| Provider dashboard: delivery failed / exhausted retries | Exempt provider CIDRs from limit_req_zone key | IPs rotate often or you need automated sync from provider docs |
| Orders missing only under load; quiet hours look fine | Raise zone rate carefully and whitelist callbacks | Rate limits interact with Cloudflare, Fail2ban, or ModSecurity |
Common issues when webhooks die behind Nginx rate limits
1. Shared limit_req zone treats gateway IPs like abusive clients
Symptoms: bursts of limiting requests, excess: in the error log; access log lines for the webhook URI end in status 429; provider retry counters climb. Checkout still “works” in the browser because the customer path is a different URI or already authenticated.
2. Geo/ISP NAT makes many merchants look like one noisy IP—while real provider ranges never get an allow rule
Symptoms: you raised rate= globally and shopper traffic improved, but webhooks still 429. Nobody added the payment company’s published CIDRs. In multi-tenant or agency-hosted Australia setups, one noisy neighbour can also push a shared edge zone over budget and starve callbacks.
3. Config test passes but reload never applied the map (or the wrong server block)
Symptoms: you edited a snippet under /etc/nginx/conf.d/ yet nginx -T still shows the old zone; webhook path lives on an API subdomain that does not include the map file; SELinux/AppArmor or a container bind-mount hid the change. Cash keeps clearing; orders stay missing after “the fix.”
4. Body or signature middleware never runs because 429 is returned at the edge
Symptoms: application logs are silent for failed orders; only Nginx sees the POST. Teams waste hours debugging HMAC secrets and queue workers that never received the payload.
Issue 1 — Diagnose 429s on payment callbacks from the logs
Work on the edge host that terminates TLS for the store (or the ingress container). You need shell access and permission to read Nginx logs.
Step 1 — Confirm 429 volume on webhook paths
sudo journalctl -u nginx -n 200 --no-pager
sudo tail -n 500 /var/log/nginx/access.log | awk '$9 == 429 {print}'
sudo grep -E ' 429 |limiting requests' /var/log/nginx/access.log /var/log/nginx/error.log | tail -n 100Note the request URI, client address, and timestamp. Provider callbacks often look like POST /webhooks/stripe, /paypal/ipn, /api/payments/notify.
Step 2 — Isolate provider source IPs
sudo awk '$9 == 429 && $7 ~ /webhook|payment|ipn|notify/ {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | headCross-check those addresses against the provider’s official webhook IP documentation (download the current list; do not guess). Australian merchants commonly see US or EU egress ranges even when the shopper is local.
Step 3 — Show the active limit configuration
sudo nginx -T 2>/dev/null | grep -E 'limit_req|limit_conn|geo |map 'If limit_req_zone keys on $binary_remote_addr with a tight rate and the webhook server block applies limit_req zone=... without an exemption, you have the root cause.
When to call Fixwebnode: logs are centralised in a way you cannot query, or multiple layers (CDN + host Nginx + app) disagree on which hop issued the 429. Book a remote session via Fixwebnode website repair before peak trading.
Issue 2 — Build a native Nginx geo whitelist for provider IPs
Nginx geo builds an efficient IP→value lookup loaded at configuration time. The standard pattern is: default clients keep a real limit key; trusted provider networks get an empty key so they do not consume the zone.
Step 1 — Create a dedicated map file (example path; adjust for your layout):
sudo mkdir -p /etc/nginx/conf.d
sudo nano /etc/nginx/conf.d/payment-webhook-geo.confStep 2 — Define geo + zone (replace CIDRs with the provider’s current published ranges)
# /etc/nginx/conf.d/payment-webhook-geo.conf
# default → rate-limit by client IP; payment ranges → empty key (unlimited for this zone)
geo $payment_limit_key {
default $binary_remote_addr;
# EXAMPLES ONLY — replace with live provider docs before production
3.18.12.0/22 "";
3.130.192.0/22 "";
54.187.174.169/32 "";
54.187.205.235/32 "";
54.187.216.72/32 "";
# Add every official webhook CIDR; use /32 for single hosts
}
# 10m slab; 30r/s sustained with modest burst later in the server block
limit_req_zone $payment_limit_key zone=per_client_pay:10m rate=30r/s;Empty string keys are ignored by limit_req, so whitelisted sources bypass this zone without disabling protection for everyone else.
Step 3 — Apply the zone only where shopper abuse matters; keep webhooks covered by the same zone so the empty key takes effect
# inside the relevant server { } for api.example.com or the main shop
location = /webhooks/stripe {
limit_req zone=per_client_pay burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000; # or fastcgi_pass / php-fpm sock
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
limit_req zone=per_client_pay burst=40 nodelay;
# … normal app routing …
}If you previously used a blanket limit_req in http or a parent server, remove the duplicate so only per_client_pay (with the geo key) applies.
Step 4 — Optional explicit deny-list style using a binary flag (useful when you also branch logging):
geo $is_payment_provider {
default 0;
3.18.12.0/22 1;
54.187.174.169/32 1;
}
map $is_payment_provider $payment_limit_key {
0 $binary_remote_addr;
1 "";
}Same zone declaration as above, keyed on $payment_limit_key.
When to call Fixwebnode: you terminate TLS on a CDN and the origin only sees the CDN anycast address—geo must run where the real client IP is restored (real_ip_header / set_real_ip_from). That wiring is easy to get wrong under load.
Issue 3 — Test, reload, and prove orders stick
Step 1 — Syntax check before any signal
sudo nginx -tExpected: syntax is ok and test is successful. Fix path errors before reload.
Step 2 — Reload without dropping keepalives
sudo systemctl reload nginx
# or, if you manage the master directly:
# sudo nginx -s reloadPrefer reload over restart on production Australian trading hours so workers finish in-flight requests.
Step 3 — Confirm the running config contains your geo block
sudo nginx -T 2>/dev/null | grep -A20 'geo $payment_limit_key'
sudo nginx -T 2>/dev/null | grep 'zone=per_client_pay'Step 4 — Synthetic check from a non-whitelisted IP (should still limit under abuse)
# from a jump host that is NOT in the geo allow list
for i in $(seq 1 80); do curl -s -o /dev/null -w "%{http_code}\n" -X POST https://your-store.example/webhooks/stripe; done | sort | uniq -cYou should still see 429 appear under deliberate flood. That proves shopper protection remains.
Step 5 — Verify provider deliveries
sudo tail -f /var/log/nginx/access.log | grep -E 'webhook|payment|ipn'Trigger a small live or sandbox payment. Expect 200 or 204 from your app, not 429. Then confirm the order row, stock movement, and email/SMS job in the application database or admin UI.
Step 6 — Application-side sanity (Node or PHP-FPM examples)
# if the app is Node behind proxy
sudo systemctl status your-node-app --no-pager
# if PHP
sudo systemctl status php8.2-fpm --no-pager
sudo tail -n 100 /var/log/php8.2-fpm.logIf Nginx now returns 200 but the app 5xxs, the rate-limit incident is closed and you have a separate application defect—do not keep widening Nginx limits to paper over app errors.
When to call Fixwebnode: reload succeeds but provider dashboards still show failures, or only one of several webhook URLs was updated. Remote platform work for denser stacks is covered under Melbourne CBD React & Node.js enterprise platform engineering when the storefront API is a custom Node service rather than a stock CMS.
Issue 4 — Hardening so the whitelist does not become a backdoor
Whitelisting must be narrow and documented.
- Only list official webhook CIDRs; never entire cloud /8 ranges.
- Keep signature verification (HMAC secrets) in the application—geo is transport hygiene, not auth.
- Log exempted hits at info level for a week after change.
- Calendar a quarterly review when providers publish IP updates.
- If Fail2ban jails parse Nginx 429s, exclude the webhook location from ban pipelines so a mis-synced map cannot lock out the gateway mid-sale.
# example: show whether real_ip is restoring CDN edge addresses
sudo nginx -T 2>/dev/null | grep -E 'real_ip_header|set_real_ip_from'
# after a test POST, compare $remote_addr in a debug log formatWhen DIY is enough vs when to book Fixwebnode
DIY is enough when you have root on a single Nginx host, one clear webhook path, provider IP docs in hand, and a maintenance window to nginx -t + reload. Follow the numbered steps above, keep a copy of the previous config, and watch the first twenty live callbacks.
Book Fixwebnode when any of these apply: TLS terminates on a CDN or load balancer and client IPs are wrong at origin; several microservices share zones; you cannot risk reload during national sale windows; 429s alternate with 499/502 under upstream pressure; or order loss is already a finance reconciliation problem. Fixwebnode is a direct specialist provider—not a freelance marketplace—working remote across Australia. See where support is offered on the service areas page, then continue on the repair landing page for a practical booking conversation.
Close the gap between captured payments and stored orders
A store that pulls cash while dropping order data is usually failing at the edge long before cart logic runs. Nginx logs showing HTTP 429 on payment webhooks are definitive: rate limits treated the provider like abusive traffic. A native geo map that empties the limit_req key for official provider CIDRs, a clean nginx -t, and a graceful reload restore callback delivery without abandoning shopper protections.
If you want a specialist to implement, review CIDRs, and watch the first production webhooks with you, start a conversation at https://fixwebnode.com.au/website-repair-australia. Bring provider IP docs and a sample failing request ID—we will meet you at the log line that proved the outage and leave you with a whitelist you can maintain.