xmlrpc.php with login attempts. One is the provider's problem and the other is yours to fix, so it pays to tell them apart before anyone upgrades a plan.
This comes up often in support. A ticket says the site is slow, the graph says CPU is maxed, and the customer asks for a bigger plan. Two quick checks usually settle which way to go: the st value in top, and a count of POST requests to xmlrpc.php in the access log.
Start with the CPU breakdown, not the load average
%Cpu(s) row near the top of top. It splits CPU time into categories: us (user processes such as PHP and MySQL), sy (kernel work), ni (niced processes), id (idle), wa (waiting on disk I/O), hi and si (hardware and software interrupts), and st.
st is steal time. On a virtual machine, your virtual CPUs are scheduled by the hypervisor onto physical cores that other guests also use. Steal is the share of time your vCPU was ready to run but the hypervisor was busy running someone else. Your kernel can see the gap, but nothing inside your VM caused it.
Inside top, pressing 1 shows each vCPU on its own line, which helps because steal is often uneven across cores. Pressing P sorts processes by CPU usage. Since top is a moving snapshot, a few seconds of recorded samples are easier to read:
BASH
# One sample per second, ten samples. The last column, st, is steal time.
vmstat 1 10
# Per-CPU breakdown including %steal (requires the sysstat package)
mpstat -P ALL 1 5vmstat, the CPU columns on the right are us, sy, id, wa, and st. The first line is an average since boot, so read the lines after it.
What each cause looks like on the server
xmlrpc.php shows up as real work. us is high, id is near zero, and st stays low. The process list is full of PHP workers burning CPU: php-fpm on Nginx or Apache with FPM, lsphp on LiteSpeed, or apache2 / httpd processes under mod_php. MySQL is often near the top too, because every login attempt has to look up a user and check a password hash. Your server is doing exactly what it was built to do. The work just comes from nobody you want.
Steal looks different. st is noticeably elevated and stays there across many samples, while us is moderate and the process list does not explain how slow things feel. Everything is sluggish, not only the website: typing over SSH lags, cron jobs take longer, wp-cli commands crawl. Traffic in the access log looks normal.
There is no universal number where steal becomes a problem. As a rule of thumb, small amounts of occasional steal are common on shared hypervisors. Steal that stays high while your own usage is modest is the signal worth acting on.
One detail trips people up. Steal only accumulates when your VM actually wants CPU. An idle VM on a crowded host shows almost no steal, because nothing is waiting. So steal tends to rise exactly when your traffic rises, which is why a busy period plus a noisy neighbor can look like a single problem. Measure during the slowdown, not an hour later.
Also note that on container-based virtualization such as LXC or OpenVZ, the guest shares the host kernel, and st may read zero all the time or not reflect contention at all. If that is your platform, steal is not a reliable signal and you will need to ask the provider what they can see.
Count POST requests to xmlrpc.php
/var/log/nginx/access.log on Nginx, /var/log/apache2/access.log on Debian and Ubuntu Apache, /var/log/httpd/access_log on RHEL-family systems, and per-domain logs under a control panel's own log directory. Adjust the path below to match yours.
BASH
# Total POST requests to xmlrpc.php in the current log
grep -c 'POST /xmlrpc.php' /var/log/nginx/access.log
# Top client IPs sending them
grep 'POST /xmlrpc.php' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# Requests per minute, to line up with the slowdown
grep 'POST /xmlrpc.php' /var/log/nginx/access.log | awk '{print $4}' | cut -d: -f1-3 | sort | uniq -c | tail -30access.log.1 if the slowdown started before the last rotation.
The per-minute output is the most useful one. If POST counts jump in the same minutes where vmstat showed high us and low st, you have your answer. If the slow minutes show ordinary traffic and high st, the cause is outside your VM.
Request counts also understate the attack. XML-RPC supports system.multicall, which lets one request carry many method calls, so a single POST can test a large batch of passwords without triggering the rate limits or CAPTCHA that usually protect wp-login.php. Each request still boots WordPress fully, which is why even a modest request rate can saturate PHP workers. Spotting multicall patterns in more detail is covered in Spotting XML-RPC Multicall Attacks in Web Server Access Logs.
A handful of xmlrpc.php requests is normal. Pingbacks, Jetpack, and some mobile publishing apps use it. A steady stream from a few IPs, or from many IPs at a uniform rate, is not.
Red herrings worth ruling out
wp-login.php does not mean there is no brute-force attempt. Bots moved to xmlrpc.php precisely because it skips the protections on the login page.
HTTP 200 on those POST lines does not mean the attacker got in. XML-RPC typically returns 200 with a fault message inside the body for a failed login. Why HTTP 200 in Access Logs Does Not Mean an Exploit Succeeded explains why status codes mislead during triage.
High wa points at disk, not CPU. If wa dominates and both us and st are low, neither of these two causes is the main story, and slow storage or a heavy query deserves a look first.
Upgrading the plan muddies both diagnoses. More vCPUs let a bot flood run a bit longer before the site falls over, and whether a resize moves you off a crowded host depends entirely on the provider.
Finally, both can happen at once. A bot flood makes your VM demand more CPU, and on a busy host that extra demand shows up as more steal. Fix the part under your control first, then see what steal looks like on normal traffic.
Confirming the root cause
xmlrpc.php and watch the numbers. Most modern WordPress sites do not need XML-RPC, since the REST API has largely replaced it. Check first whether the site relies on Jetpack, the WordPress mobile app, or a plugin that still uses it, because blocking will break those.
On Apache 2.4, this goes in the site's .htaccess or virtual host:
APACHE
# Deny all requests to xmlrpc.php (Apache 2.4 syntax)
<Files xmlrpc.php>
Require all denied
</Files>location = /xmlrpc.php { deny all; }. Run apachectl configtest or nginx -t before reloading so a typo does not take every site down. On LiteSpeed, behavior with .htaccess depends on the edition and configuration, so confirm the rule takes effect.
If you had the right diagnosis, the access log starts showing 403 for those POSTs, PHP workers drop off the top of the process list, and us in vmstat falls within a minute or two. If us barely moves, the bots were not the main load.
For the steal case, there is nothing to fix inside the VM. What helps is evidence. Run vmstat -t 5 during slow periods so each sample carries a timestamp, save the output, and pair it with access log counts from the same window showing traffic was normal. A provider can act on timestamped steal measurements much more easily than on a general complaint that the server feels slow.
The practical habit is simple: before recommending more CPU, look at st and count POSTs to xmlrpc.php for the same few minutes. High user time with a flood of XML-RPC requests is your problem and usually a one-line fix. High steal with ordinary traffic is the host's problem, and the right move is to gather timestamps and escalate rather than pay for a bigger plan on the same crowded hardware.