abuse-guard: Auto-ban abusive clients by error-response rate (404/403/5xx)
Requires the Pro plan (or higher) of the GetPageSpeed NGINX Extras subscription.
Installation
You can install this module in any RHEL-based distribution, including, but not limited to:
- RedHat Enterprise Linux 7, 8, 9 and 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 and Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-abuse-guard
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-abuse-guard
Enable the module by adding the following at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_abuse_guard_module.so;
This document describes nginx-module-abuse-guard v1.0.0 released on Jun 23 2026.
Your error log is a confession. Abuse Guard reads it in real time and shuts abusers out.
Every scanner, fuzzer, and credential-stuffing bot leaves the same fingerprint:
a spray of 404s hunting for hidden paths, 403s rattling locked doors, failed
request after failed request. Abuse Guard watches the status codes your server
actually returns, identifies the clients whose traffic is mostly failure, and
locks them out — decided inside the NGINX worker, on the request itself, in a few
microseconds. No sidecar. No log shipper. No scripting layer. Just compiled C
doing one job exceptionally well.
Spec sheet
| Trigger | Per-client rate of error responses you choose (403/404 by default) |
| Action | Timed lockout — a hard ban for a fixed window, not a throttle |
| Decision point | NGINX preaccess phase, before any handler or upstream runs |
| Memory model | Fixed bytes per client, independent of threshold → botnet-scale |
| Fleet mode | Optional ban replication across nodes via Redis / Valkey |
| Durability | Optional on-disk ban snapshots that survive reloads and reboots |
| Footprint | One self-contained module; zero runtime dependencies by default |
| Platforms | RHEL / AlmaLinux / Rocky / CentOS Stream / Oracle / Amazon Linux |
| ## |
The problem it removes
Legitimate visitors almost never generate a burst of errors. Abusers generate
little else — that asymmetry is the whole game. A vulnerability scanner walking
your tree is a wall of 404s. A bot poking admin endpoints is a wall of 403s.
A brute-force run is a wall of failures.
Rate limiters treat that traffic like any other: they slow everyone by request volume and let the offender right back in the instant it eases off. Abuse Guard does the opposite. It ignores well-behaved traffic entirely and reserves its one response — a real, time-boxed ban — for clients defined by their errors.
Use a rate limiter to shape load. Use Abuse Guard to evict abuse.
How a ban is decided
Three moving parts, all inside the worker:
1 · A leaky score, per client. Every client identity carries a single small
number in shared memory. Each matching error adds to it; the score bleeds away
continuously at threshold ÷ interval per second. A short, sharp burst pushes it
over the line; a slow trickle never does. Crucially, that score is one fixed-size
record no matter how high you set the threshold — so a single zone comfortably
tracks the tens of thousands of distinct source addresses a botnet throws at you.
2 · A hard deadline. The moment the score crosses your threshold, the client
earns a blocked_until timestamp. Until then it is simply gone — every request
turned away at the preaccess phase, before NGINX spends a cycle on routing,
files, or upstreams. The rejection is the cheapest possible outcome.
3 · A privacy-correct refusal. Banned clients receive 429 Too Many Requests
(your choice of code) tagged so no shared cache can ever store it and serve one
client's punishment to another, with a Retry-After telling honest clients when
to return.
Identities are folded into a fixed-size digest, so keying on something fat like
$request_uri or a header costs exactly as much memory as keying on an IP.
Live in under a minute
Abuse Guard ships as a precompiled, signed module from the GetPageSpeed repository — drop it in, no build toolchain required.
sudo yum -y install https://extras.getpagespeed.com/release-latest.rpm
sudo yum -y install nginx-module-abuse-guard
Wire it up:
load_module modules/ngx_http_abuse_guard_module.so;
http {
abuse_guard_zone zone=clients:10m; # one shared-memory zone
server {
location / {
abuse_guard zone=clients; # enforce here
}
}
}
sudo nginx -t && sudo systemctl reload nginx
Those defaults ban any IP that returns 100 403/404 responses inside a
5-minute window, for one hour. Tighten or loosen every number below.
Configuration
Abuse Guard is four directives. The first declares a policy; the rest apply it, exempt people from it, and (optionally) share it across machines.
Declare a policy — abuse_guard_zone
An http-level directive. It carves out one shared-memory zone and sets the
policy that governs it. Set as many or as few knobs as you like — the zone's
name and size are the only thing you must provide; sensible defaults fill in the
rest (the values shown below are exactly those defaults).
abuse_guard_zone zone=clients:10m ← name + size (the only must-have)
key=$binary_remote_addr ← who is "one client"
statuses=403,404 ← which responses count as errors
interval=300s ← the scoring window
threshold=100 ← errors in that window → ban
block=60m; ← how long the ban holds
zone=clients:10m is the policy's identity and budget: a name you reference
from abuse_guard, and the shared-memory size. About 10 MB tracks on the order of
a hundred thousand live clients.
Everything else is optional tuning:
key— the expression that defines a single client. Any NGINX variable; the default$binary_remote_addrkeys on source IP. A request whose key comes out empty is skipped entirely (handy with amap, below).statuses— the response codes that count as errors: individual codes, ranges, or a mix, e.g.statuses=401,403,404,500-599. Defaults to403,404.interval— the window the score decays over (default300s). A burst inside it trips a ban; a slow trickle spread wider never accumulates.threshold— how many errors within that window cross the line, up to 1024 (default100).block— how long a tripped client stays locked out (default60m).inactive— how long a dormant client lingers in memory before it's reclaimed (defaultmax(1h, interval, block); any explicit value must be at least as large as bothintervalandblock).redis—onto replicate this zone's bans across a fleet (see below);offby default.persist— a file path to snapshot bans into so they survive a restart.persist_interval— how often that snapshot is rewritten (default5s).persist_secret— a hex key that signs the snapshot with HMAC-SHA256, so a tampered file is rejected rather than loaded.
Why
5xxis left out by default: a server error is usually your side's doing, and counting it would let one flaky backend get innocent visitors banned. Addstatuses=403,404,500-599only when you deliberately want to act on clients that trigger server errors.
Apply it — abuse_guard
Valid in http, server, and location blocks, so you can guard a whole site
or just the endpoints that attract abuse. Name the zone to switch it on; write
abuse_guard off; in a nested scope to switch it back off.
location /wp-login.php {
abuse_guard zone=clients status=429 log_level=warn;
}
zone— the zone (declared above) whose policy applies here.status— the code a banned client receives, anywhere in400–599(default429).dry_run—onto observe without enforcing: the verdict is logged but no ban is written. Off by default.log_level— how loudly to log each decision:info,notice(default),warn, orerror.
Roll out fearlessly with dry_run=on. It records every ban it would issue
without touching state, so you can calibrate thresholds against live traffic — even
next to an enforcing location on the same zone — then flip it live.
Exempt the good guys — abuse_guard_allow
Context: http · server · location · repeatable, inherited downward.
abuse_guard_allow 127.0.0.0/8;
abuse_guard_allow 10.0.0.0/8 192.168.0.0/16;
Listed clients are never counted and never banned. Matching is on the true
connection address, so it cooperates with realip. This is also how you protect
verified search crawlers: allow the published Googlebot / Bingbot ranges so a
bot grinding through stale URLs (and racking up 404s) is never caught.
Share bans across the fleet — abuse_guard_redis
Context: http
abuse_guard_redis host=10.0.0.5 password=… ; # tls://host for TLS
abuse_guard_zone zone=clients:10m redis=on;
Point every node at one Redis or Valkey, flip redis=on, and a ban earned on any
machine propagates to all of them. Defaults: port=6379, db=0, prefix=ag_,
timeout=100ms. How it stays fast is the next section.
One ban, every node — without slowing a single request
Behind a load balancer, a per-server ban is theatre: the attacker just lands on a different node. Abuse Guard closes that gap without ever putting Redis in the request path.
Each node decides locally and counts locally. The instant it issues a ban, it broadcasts that one fact to the cluster and records a durable copy. Every other node imports it within milliseconds, and any node that was offline reconciles the moment it reconnects. Because enforcement is always served from each node's own in-memory state, a visitor's request never waits on a network round-trip — the only cost of clustering is that a freshly-banned attacker is shut out fleet-wide a heartbeat later instead of instantly.
Redis here is a one-way alarm bell, not a shared ledger consulted per request — so a slow or missing Redis can never add latency to your traffic. Run it on a private network and treat it as privileged: anything that can write to it can issue bans.
Bans that outlive a restart
Point a zone at a file and active bans are snapshotted on an interval and restored at startup, so a reload or a reboot doesn't hand every attacker a fresh slate.
abuse_guard_zone zone=clients:10m
persist=/var/lib/nginx/abuse_guard/clients.state
persist_secret=00112233445566778899aabbccddeeff;
The snapshot is integrity-checked, written so a crash can never leave a torn file,
and — with persist_secret — cryptographically signed so a tampered file is
rejected rather than trusted. Keep the directory readable only by the worker user.
See everything it decides
Three variables expose Abuse Guard's verdict to your logs and config:
| Variable | Value |
|---|---|
$abuse_guard_status |
BYPASSED · PASSED · COUNTED · BLOCKED · DRY_RUN |
$abuse_guard_count |
Errors currently attributed to this client. |
$abuse_guard_blocked_until |
Unix time the ban lifts, or 0. |
log_format guard '$remote_addr "$request" $status '
'guard=$abuse_guard_status count=$abuse_guard_count';
Keying behind a CDN or proxy? Never trust a raw X-Forwarded-For. Let
realip resolve the real client first, then key on $binary_remote_addr:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Need per-request exemption logic? Any request whose key resolves to an empty
string is ignored — so a map lets you, say, track anonymous visitors by IP while
leaving authenticated users untouched.
Engineered to be trusted in production
Abuse Guard is held to a standard far above "it compiles." Every change runs the gauntlet of AddressSanitizer, UndefinedBehaviorSanitizer, Valgrind, static analysis, and continuous fuzzing of its parsers and on-disk format. Its optional dependencies — clustering and signed snapshots — are best-effort by design: if Redis or the disk misbehaves, enforcement quietly carries on from local memory. Your traffic is never held hostage to a dependency.
Get Abuse Guard
Abuse Guard is a commercial NGINX module from GetPageSpeed LLC, delivered with ongoing updates and support through a GetPageSpeed subscription.
- Browse the full NGINX module catalog → https://nginx-extras.getpagespeed.com/modules/
- Licensing, volume deployments, or a hand getting set up → getpagespeed.com/contact-us
© GetPageSpeed LLC. All rights reserved.