
Every hosting operator knows this ticket: “My site was slow last night.” You check the graphs, and there’s the nightly malware scan, sitting right on top of the slowdown. The tempting fix is to scan less often, or to turn the scanner off. Either way, you’ve traded a slow night for a longer window in which an infection can go unnoticed.
There’s a better option. Most of the damage comes from a few specific causes, and each one has its own fix.
What actually slows the server down
A scanner reads every file and analyzes its content. That creates pressure in three places:
- CPU: a scanner running one thread per core competes with PHP and MySQL for every one of them. Visitors feel this as slower page generation.
- Disk: reading millions of small files means a lot of seeks and a busy queue. On shared storage or slower disks, this is often worse than the CPU load, and it’s easy to miss because
topshows the CPU as idle while everything waits. - Page cache: the kernel keeps recently read files in memory. A full scan reads a lot of files that nobody has asked for, which can push out the ones your sites actually use. The slowdown can then outlast the scan, until the cache warms up again.
Which one hurts depends on the server, so measure before changing anything.
Measure it first
Run these while a scan is active, and compare with a quiet period. pidstat and iostat come from the sysstat package.
# Is the CPU busy, or is it waiting on disk? Watch the "wa" column.
vmstat 5
# How busy are the disks? Watch %util and the await columns.
iostat -x 5
# What is the scanner itself using? CPU, memory and disk, every 5 seconds.
pidstat -u -r -d -p SCANNER_PID 5
Then measure what visitors feel. Take a real page on the server and time it during a quiet period and during a scan:
curl -o /dev/null -s -w '%{time_starttransfer}\n' https://example.com/
How to read the results:
| What you see | The problem is probably | Start with |
|---|---|---|
High CPU use, low wa |
CPU contention | A hard CPU limit |
High wa, disks near 100% |
Disk I/O | I/O priority, fewer files scanned |
| Page times stay high after the scan ends | Page cache eviction | Fewer files scanned |
| Page times don’t change | Nothing | Leave it alone |
A high load average on its own proves little, because on Linux it also counts tasks waiting on disk.
What each setting really does
These work with any scanner. They are often mixed up, and the difference matters.
nice sets priority, not a limit. A process with nice -n 19 gives way when something else needs the CPU, but it will happily use every core on an idle machine. That protects your sites reasonably well, yet the graphs still show 100%, and a customer on a VPS or a metered plan may still object.
ionice sets disk priority, and it depends on the kernel’s I/O scheduler. Check which one is active. The one in brackets is in use:
cat /sys/block/sda/queue/scheduler
The priority classes are honored by schedulers like bfq. With mq-deadline or none, which are common on virtual machines and NVMe, they may do little or nothing, so measure again instead of assuming it worked.
A CPU quota is a hard limit. With systemd, the kernel enforces it through cgroups, whatever the process wants:
systemd-run --scope -p CPUQuota=50% nice -n 19 ionice -c 2 -n 7 YOUR_SCANNER /home
Here, 50% means half of one core, and ionice -c 2 -n 7 is the lowest normal disk priority. The idle class (-c 3) looks tempting, but under bfq it only gets disk time when nothing else wants it, which can stall a scan on a busy server. The scan takes longer, but the ceiling is real. Set the limit too low on a server with many accounts, though, and a nightly scan may still be running the next morning.
Scan fewer files
The cheapest read is the one you don’t do.
Exclude with care. Mail stores and rotated logs are usually reasonable to skip if your scanner only targets web content. Don’t exclude wp-content/uploads or cache folders because they are large. Those are favorite hiding places for malware.
Stagger schedules. If 50 servers sit on the same storage or hypervisor, don’t start them all at 03:00.
Scan by change. A full daily scan mostly re-reads files that haven’t changed since yesterday. Between full scans, you can target recent changes:
find /home/*/public_html -type f -name '*.php' -ctime -1
This is a good incremental pass, but keep running full scans too. New signatures can catch a file that was already there and that nothing has touched.
How we built this into our scanner
Customers told us our own server-wide scan was showing 100–400% in top. They were right to complain, so we rebuilt how Pyxsoft Anti Malware uses the machine. Here’s what it does now.
A CPU limit that counts the whole process. Before each unit of work, the scan workers check the CPU time the entire process has actually used, as reported by the kernel, and pause if it is over budget. Since it counts the whole process, indirect work like garbage collection is included too, which is why the number matches what you see in top.
Low priority by default. The service runs with Nice=10 and a low I/O priority, on top of the limit, so it gives way to your sites.
No repeated work. A file that hasn’t changed since it was last checked isn’t analyzed again, and a file that changes is.
The numbers
Measured on a modest server with other services running: 2 vCPU Xeon, 3.5 GB RAM, a virtual disk and production signatures. “Cold” means the first scan, with none of the files in the system’s page cache. “Warm” means a second scan with nothing changed. Each value is the median of three runs.
| Site | Files | Cold | Warm |
|---|---|---|---|
| WordPress with 7 plugins (231 MB) | 9,229 | 12 s | 0.5 s |
| WordPress with 15 popular plugins and a theme (596 MB) | 30,466 | 32 s | 0.9 s |
And here’s what the CPU limit costs in scan time, cold:
| CPU limit | 9,229 files | 30,466 files | Average CPU observed |
|---|---|---|---|
| No limit | 6 s | 16.7 s | 150% (2 threads) |
| 100% (default) | 12 s | 32 s | 80% |
| 50% | 20 s | 52 s | 50% |
| 25% | 44 s | 120 s | 25% |
Memory during a scan was 150–200 MB cold with the default limit, and 90–115 MB warm. At rest, the service used 75 MB and averaged 0.3% CPU.
What these numbers don’t promise
- They are from one server and two sites. Yours will differ, especially on disk speed.
- They measure the scanning library with the product’s settings, not the entire service. Reports and emails add some work.
- Half a second applies to files already checked with the same signatures. After a signature update, files are checked against the new part only, so that scan takes longer.
- A second account with the same WordPress saves little. In our test, it went from 4.4 s to 2.8 s.
- This isn’t a comparison. We haven’t tested other products on this hardware.
Check it yourself
You don’t need to take our numbers on trust. Install it on a test server, start a server-wide scan and watch it:
curl -s https://www.pyxsoft.com/install-anti-malware | bash
top -p "$(pidof pyxsoft)"
Change the CPU limit in the settings in WHM, and the number in top should follow. Run the curl timing from above while it scans, too. That’s the number your customers notice.
It is licensed per server, whatever the number of accounts, and you can try it free for 7 days without a credit card.
If a scan does turn something up, our cleanup guide for cPanel servers covers what to do next.