Obstensibly, this blog has always been a developer's blog of a front-end developer, UX developer , I mean, a full-stack developer's personal blog, even if 95% is Mac-related nonsense. However, now that I'm a lead developer, I've accidentally been writing about development projects more frequently as I've wrote a GUI wrapper for an Xbox Utility, How to install Pihole via Docker, wrote a plugin for auth0 for all, created a secure self hosted http based file sharing site and such.

Sadly, I'm behaving like an actual developer. Anyhow, disappointing as that is, I've written a WordPress plugin for Rate limiting.

Stop bad requests from mucking up your WordPress

Wordpress Rate Limiter Plugin


My current company has one hell of a knot to untangle as we look to decouple 140+ wordpress websites in a single multi-site. It's a major curse, and it means that if one of your sites within the Multi-site gets hammered with stupid bot requests, it can affect all the other sites. This is where the WordPress Rate Limiter plugin comes in handy.

What this plugin actually does

Network Rate Limiter watches a few high-risk WordPress endpoints and slows or blocks abusive clients per IP. It’s designed for multisite networks, but it works on single sites too. The idea is clamp down on bad traffic while allowing legitimate users to access the site without interruption. It's not sophisticated, for rotating IP spam but really stops a lot of the agent based behavior that can cause issues. This is more of an issue thanks to AI.

  • Protected endpoints: /wp-login.php, /xmlrpc.php, /wp-admin/admin-ajax.php, and anything under /wp-json/.
  • Safe by default: logged-in admins, WP-Cron, Site Health, and HTTP OPTIONS/HEAD requests are skipped.
  • Time-aware: you choose a “daytime” window with stricter limits; outside of it, limits relax automatically.
  • Progressive: repeat violators get longer blocks (exponential backoff) that decay after a probation period.
  • Multisite-friendly: network-wide defaults with per-site overrides.

How rate limiting is counted (no hand-waving)

Instead of a brittle “one counter per minute,” the plugin uses a two-bucket approximation of a sliding window:

  1. One bucket counts requests in the current minute.
  2. Another holds the previous minute’s count.
  3. The final score is current + a time-weighted slice of the previous bucket (the part that still overlaps the last 60s).

Result: spikes right after a reset still count, and legitimate traffic isn’t penalized by harsh window edges. This keeps easy interval detection at bay from a bot's perspective.

What triggers a block

  • Per-endpoint thresholds: each endpoint/method has a soft and hard limit per minute. At night, thresholds double.
  • Soft limit: returns 429 and starts exponential backoff for that IP.
  • Hard limit: immediate block (longer backoff).
  • Global clamp: if an IP is hammering any mix of protected endpoints overall, it’s blocked even if no single endpoint trips.

Backoff math: blocks start short (e.g., 2 minutes) and double on each violation, up to a max (e.g., 60 minutes). The “violation score” auto-expires after your configured probation window, so well-behaved IPs cool off. I imagine people will fork this and change this behavior on their own.

Legit traffic stays legit

  • Verified search engines: Google/Bing are allowed only if both checks pass:
    1. User-Agent hints it’s the right bot (e.g., Googlebot, bingbot), and
    2. Reverse DNS ends with an expected domain (.googlebot.com, .search.msn.com) and forward DNS resolves back to the same IP.
    Cached per site and per IP for up to 7 days (subject to transient eviction). UA must still match on each request.
  • Allowlists you control:
    • IP or CIDR (IPv4/IPv6)
    • Reverse-DNS suffixes with forward confirmation
    • User-Agent substrings (use with care; UAs are spoofable)
    • Specific REST prefixes (under /wp-json/)
    • Specific admin-ajax actions (e.g., heartbeat)
  • Secret header bypass: give your monitors a header like X-NetRL-Bypass: your-long-random-token and they’ll skip the limiter.

It comes prepopulated with commonly "good" bots whitelisted, like Google, Bing, various SEO tools and so on.

What clients see (headers)

Every protected response includes standard rate-limit hints:

  • X-RateLimit-Limit – the current hard limit
  • X-RateLimit-Remaining – how many requests are left in the window
  • X-RateLimit-Window – window duration in seconds (60)
  • On block: Retry-After and X-RateLimit-Reset (epoch timestamp)

Multisite behavior

  • Network Defaults: set once in Network Admin; good baseline for all sites.
  • Per-site Settings: each site can override (or inherit where left blank).
  • Per-IP scope: counters and blocks are tracked per site; the “global clamp” is per IP within a site’s protected endpoints.

Operational notes

Nerd stuff, you can skip this unless you really care.

  • Atomic counters with object cache: Redis/Memcached make increments race-safe. Without them, the plugin falls back to transients (fine for “soft” protection).
  • Logging: optional JSON lines go to the PHP error log (blocks, and optionally bypasses) and a netrl_log_event action fires for shipping to APM/log pipelines.
  • Timezones: daytime hours use the site’s timezone. If none is set, it defaults to America/Los_Angeles.
  • Security of client IP: the plugin prefers CF-Connecting-IP, X-Real-IP, then X-Forwarded-For (left-most public), then REMOTE_ADDR. Ensure your proxy/CDN is trusted before honoring headers.

Why this approach?

There are a zillion ways to do rate limiting. This plugin aims for a practical balance of effectiveness, simplicity, and low friction for legitimate users. It’s not perfect, but it works well in practice and stopped an outage that seemed to be occuring weekly. While our AppDex scores are still terrible, they're remarkably less terrible.

  • Resilient under bursts: the two-bucket window smooths edge cases without heavy math.
  • Fair to humans and APIs: daytime/night profiles match your traffic patterns.
  • Practical ops: easy allowlists, real bot verification, headers for observability, and simple logs/hooks.
  • Safe defaults, sane overrides: works out-of-the-box; tune it as you learn your traffic.