Bali, Indonesia
Insights

Rate Limiting xmlrpc.php to Prevent Recurring Server Load Spikes

October 15, 2026
0:00
A sudden CPU spike on a WordPress server rarely comes from a sudden wave of genuine visitors reading your blog posts. More often than not, checking the active web server connections reveals dozens of simultaneous HTTP POST requests targeting a single file: xmlrpc.php. Because each incoming request triggers a full PHP bootstrap and multiple database queries, a modest flood of automated requests can saturate every available PHP-FPM worker in seconds. At the hosting support desk, this pattern often leads to confusion. Server owners notice load alerts climbing from nominal levels to high double digits, assume they are undergoing a distributed denial-of-service attack, and immediately look into upgrading their VPS plan. Scaling the server rarely fixes the underlying issue. The botnet simply scales its request rate to fill whatever capacity you add. To keep the server stable, the requests must be throttled or filtered long before they ever reach the PHP runtime.
XML-RPC is an older remote procedure call protocol that enables external applications to interact with WordPress using XML-encoded payloads over HTTP. In the earlier days of WordPress, it powered desktop blog editors, mobile apps, pingbacks, and trackbacks. While modern WordPress development has largely shifted toward the REST API, xmlrpc.php remains enabled by default across standard installations for backwards compatibility. Automated credential-stuffing tools favor this endpoint because of an XML-RPC feature called system.multicall. In a normal login attempt via wp-login.php, an attacker sends one set of credentials per HTTP POST request. With system.multicall, an attacker can bundle hundreds of authentication attempts into a single HTTP POST request. When the web server hands that request to PHP-FPM, PHP must parse the XML payload, initialize the WordPress database layer, and loop through hundreds of password-hashing evaluations (typically using bcrypt or phpass) within a single execution cycle. Password hashing algorithms are intentionally compute-intensive to prevent brute-force attacks. When ten or twenty concurrent bots send multicall requests simultaneously, the server CPU hits 100% almost immediately. Each PHP-FPM child process remains occupied for hundreds of milliseconds or even seconds while churning through the password checks. Once the pool reaches its pm.max_children ceiling, any new incoming request—including those from regular visitors browsing your homepage—gets queued up until it hits a timeout. This is often the quiet culprit behind sudden gateway errors, as detailed in our guide on reading Nginx and PHP-FPM logs to trace 502 errors.
The most common piece of advice found in community forums is to block xmlrpc.php completely. Dropping a deny all; directive in Nginx or an Apache .htaccess rule that returns a 403 Forbidden code certainly stops PHP-FPM exhaustion. If your site is completely self-contained and you manage everything through the standard wp-admin dashboard, outright blocking is an effective and zero-overhead solution. However, a complete block is not universally applicable, which is where many site administrators run into trouble. Several widely used integrations still rely on XML-RPC communication. The most prominent example is Automattic's Jetpack plugin, which uses the endpoint to sync site data, monitor uptime, and publish posts remotely from WordPress.com. Similarly, third-party syndication platforms, automated social media cross-posters, and certain native mobile publishing apps depend on XML-RPC calls. If you block the endpoint entirely on a site that depends on these tools, administrative workflows break silently. Jetpack drops its connection, mobile drafts fail to publish, and external webhooks begin returning client errors. This leaves system administrators in a dilemma: leave the door wide open and suffer periodic CPU spikes, or lock it completely and break necessary services. Rate limiting provides the balance by allowing legitimate, occasional API calls to pass through while choking the high-frequency bursts generated by automated scanners.
When applying rate limiting, the goal is to evaluate and reject excess requests directly in memory at the web server layer using lightweight event loops, preventing the backend PHP interpreter from ever spawning a worker process for discarded traffic. In Nginx, this is handled using the ngx_http_limit_req_module. The module uses the leaky bucket algorithm to enforce average request rates while permitting controlled bursts. We define a shared memory zone in the http block of the Nginx configuration, track requests by the client's IP address ($binary_remote_addr), and apply the restriction specifically to requests targeting xmlrpc.php.
NGINX
# Place inside the http {} block of nginx.conf
limit_req_zone $binary_remote_addr zone=xmlrpc_limit:10m rate=2r/s;

# Place inside the server {} block for the WordPress site
location = /xmlrpc.php {
    limit_req zone=xmlrpc_limit burst=5 nodelay;
    limit_req_status 429;

    # Forward legitimate traffic to the existing PHP-FPM fastcgi block
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_param SCRIPT_FILENAME document_rootfastcgi_script_name;
}
In this configuration, the memory zone xmlrpc_limit allocates 10 megabytes of RAM to store tracking states for client IP addresses (10 megabytes can hold roughly 160,000 distinct IP addresses). The rule permits a baseline of 2 requests per second. The burst=5 parameter allows a brief spike of up to five requests to accommodate batch synchronization from services like Jetpack. The nodelay directive tells Nginx to process legitimate burst requests immediately without introducing artificial latency, but to drop any request exceeding the burst limit instantly with an HTTP 429 (Too Many Requests) response. Because Nginx rejects the excess traffic before invoking FastCGI, an attacker sending twenty requests per second will see eighteen of them dropped with negligible CPU impact on the host.
Before deploying rate limits directly to a critical production site, it is best to verify both the passing behavior and the blocking behavior on a staging environment or a test VPS. The most straightforward verification involves sending a controlled sequence of POST requests using a terminal loop to see if the HTTP status transitions from 200 to 429 as intended. From a remote workstation or terminal, you can send an empty or minimal POST request to the endpoint multiple times in rapid succession using curl:
BASH
# Send 10 rapid POST requests to observe the HTTP status code response
for i in {1..10}; do 
  curl -s -o /dev/null -w '%{http_code}
' -X POST https://example.com/xmlrpc.php
done
If the rate limiter is configured correctly with a baseline of 2 requests per second and a burst of 5, the terminal output should show the first few requests returning the normal response code (often 200 or 405 depending on whether valid XML was sent), followed immediately by consecutive 429 status codes for the remainder of the burst. Next, examine the Nginx error log (typically located at /var/log/nginx/error.log). When a request is throttled by the leaky bucket algorithm, Nginx writes an entry indicating that the limiting zone was exceeded, noting the client IP address and the specific request path. If your error log shows these entries while your PHP-FPM status page (pm.status) shows active worker counts remaining calm and stable, the filter is doing its job.
Rate limiting is not a silver bullet for every security concern, but it is an operational control that restores stability to servers constantly disrupted by background automated botnets. Once implemented, recurring server load spikes originating from single-IP brute force attempts typically disappear from your monitoring dashboards. For sites that have a predictable list of external services accessing XML-RPC (for instance, if you only use Jetpack or access the site from a static office IP), an even more resilient architecture is combining rate limiting with an IP allowlist. You can configure Nginx or Apache to permit unthrottled access to known IP ranges, apply a strict rate limit to unknown public IPs, and drop everything else. Taking the time to observe access patterns in your logs will show you whether your installation can be locked down strictly or if a flexible rate-limiting policy is the right middle ground for your stack.