How to Speed Up & Secure a Plesk VPS: The Complete Guide
Everything I learned fixing a crashing server — from OOM kills and MariaDB bloat to Redis caching, PHP-FPM tuning, and Cloudflare WAF rules that actually work.
I run a web hosting and development business with around 37 websites spread across a single Plesk VPS. For a few days in April 2026, that server was crashing repeatedly — nine reboots in two days, backups failing, databases going down, sites returning errors. What started as a straightforward backup failure investigation turned into a full server optimisation deep-dive.
This guide documents everything I did, in the exact order I did it. If your Plesk server is slow, crashing, or getting hammered by bots, this should save you a significant amount of time and frustration. No fluff, no filler — just the real commands, real configs, and real results.
📋 Table of Contents
- Diagnosing the Problem — Reading Server Logs
- Adding Swap Space — The Emergency Fix
- Optimising MariaDB / MySQL on Plesk
- PHP-FPM Tuning — The Biggest Win
- OPcache Configuration
- Installing & Configuring Redis for WordPress
- Cloudflare WAF Rules — Bot Blocking Without Breaking AdSense
- security">Plesk Security & Nginx Bot Blocking
- Final Results & Summary
1. Diagnosing the Problem — Reading Server Logs
Before you touch a single config file, you need to understand exactly what's killing your server. Guessing wastes hours. These commands will tell you the truth in seconds.
When a Plesk backup starts throwing SQLSTATE[HY000] [2002] Connection refused errors across every subscription, the instinct is to blame the backup system. But the real question is always: why can't anything connect to the database?
In my case, the answer was buried in the system journal. This is the first command you should run on any misbehaving server:
journalctl -p err..crit --since "2026-04-17" --no-pager | tail -100
What I found stopped me cold. At exactly 11:24:33 — fourteen minutes before the failed backup — the Linux OOM (Out of Memory) killer had fired and executed MariaDB:
kernel: Out of memory: Killed process 1268 (mariadbd) total-vm:16856300kB, anon-rss:1108948kB
And then it happened again at 11:45:38. And again. Nine times in two days. To see all your recent boots and crashes:
journalctl --list-boots
To check specifically for OOM kills — the kernel silently murdering your processes when it runs out of memory:
dmesg | grep -i "oom\|killed process" | tail -30
Also check what's actually using RAM right now — this one command tells you more than any monitoring dashboard:
ps aux --sort=-%mem | head -30
free -h
/var/log/plesk/panel.log — Main Plesk log
/var/www/vhosts/yourdomain.com/logs/error_log — Per-site Apache errors
/var/log/plesk-nginx/error.log — Nginx errors
Or via Plesk UI: Tools & Settings → Server Management → Server Logs
2. Adding Swap Space — The Emergency Fix
With 23GB of RAM, you'd think memory wouldn't be an issue. But without any swap space at all, a single unexpected spike can bring your entire server down in milliseconds.
Here's the thing that surprised me: my server had 23GB of RAM and was reporting only 8.7GB available before the crashes. That sounds fine — until you understand that without swap, there is zero tolerance for spikes. When Imunify360's malware scanner (rustbolit) launches with a --memory=2G flag at the same moment your WordPress sites are busy, the kernel has nowhere to turn. It doesn't wait. It kills the nearest big process — almost always MariaDB — instantly.
Swap gives the kernel a pressure valve. It's slower than RAM, but infinitely better than crashing your database:
# Create a 4GB swap file
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# Make it permanent across reboots
echo '/swapfile none swap sw 0 0' >> /etc/fstab
# Set swappiness (10 = prefer RAM, only use swap when needed)
echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -p
# Verify
free -h
For a production server, vm.swappiness=10 is ideal — the kernel will avoid using swap unless genuinely necessary, but it'll be there when things spike. After adding swap, my server survived the same Imunify360 scan that had previously killed MariaDB twice in one morning.
For servers with 8–32GB RAM, 4GB swap is a sensible safety net. You're not using it as extra memory — you're using it as insurance against crashes. If your server is regularly hitting swap heavily, that's a sign you need more RAM or to reduce your memory footprint.
3. Optimising MariaDB / MySQL on Plesk
Out of the box, Plesk often configures MariaDB with settings designed for dedicated database servers. On a shared VPS hosting dozens of sites, these defaults can hoard enormous amounts of RAM that never even get used.
The Hidden Config File Plesk Doesn’t Tell You About
When I checked the standard MariaDB config at /etc/mysql/mariadb.conf.d/50-server.cnf, nearly everything was commented out. But run this and you might find a shock:
grep -r "innodb_buffer_pool_size" /etc/mysql/
On my server, the result was:
/etc/mysql/my.cnf:innodb_buffer_pool_size = 12G
Twelve gigabytes reserved for MariaDB's buffer pool. And when I checked how much was actually being used?
mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';"
Only 549MB out of 12,288MB was occupied. MariaDB was sitting on 11.5GB of RAM it wasn't using, preventing anything else from having it. This single setting was responsible for most of my server's memory problems.
The Complete Optimised MariaDB Config
Edit /etc/mysql/my.cnf and update/add under [mysqld]:
[mysqld]
# Buffer pool - sized to actual usage, not theoretical maximum
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
innodb_log_file_size = 256M
innodb_log_buffer_size = 32M
# Performance
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
# Connections - 300 wastes ~600MB in overhead alone
max_connections = 150
thread_cache_size = 16
wait_timeout = 60
interactive_timeout = 60
# Temp tables
tmp_table_size = 64M
max_heap_table_size = 64M
# Query cache (disable on MariaDB 10.11 - causes contention)
query_cache_type = 0
query_cache_size = 0
# MyISAM
key_buffer_size = 32M
# Slow query log - find problem queries
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
mkdir -p /var/log/mysql && chown mysql /var/log/mysql
systemctl restart mariadb
free -h
The result in my case was immediate and dramatic — nearly 10GB freed the moment MariaDB restarted with the corrected buffer pool size.
This setting trades absolute write safety for significant performance. In the event of a hard server crash (power cut, not a controlled shutdown), you could theoretically lose up to one second of transactions. For WordPress sites this is an acceptable trade-off, but avoid it on financial transaction databases.
| Setting | Default (Plesk) | Optimised | Why |
|---|---|---|---|
| innodb_buffer_pool_size | 12G | 2G | Only 550MB was actually used |
| max_connections | 300 | 150 | 300 connections = 600MB overhead |
| wait_timeout | 28800s (8hrs) | 60s | Kills idle connections from 37 sites |
| query_cache_size | varies | 0 | Causes mutex contention in 10.11 |
| innodb_file_per_table | ON | ON | Easier space reclamation |
4. PHP-FPM Tuning — The Biggest Win
This is where the real damage was being done. PHP-FPM worker processes were silently accumulating in the background, each one consuming 200MB+ of RAM, and none of them ever dying.
Running ps aux | grep "php-fpm: pool" | wc -l revealed 278 active PHP workers across 37 sites. Each consuming around 200MB. That's a theoretical maximum of 55GB of RAM on a 23GB server — only survivable because they weren't all active simultaneously. When traffic spikes hit even a handful of sites at once, it was game over.
The Per-Domain Config Problem
Plesk stores individual PHP-FPM configs for each domain here:
/opt/plesk/php/8.3/etc/php-fpm.d/
When I checked max_children settings:
grep "pm.max_children" /opt/plesk/php/8.3/etc/php-fpm.d/*.conf | sort -t= -k2 -rn
The results were alarming. raretoyhub.com was set to 120 workers. That one site alone could theoretically consume 24GB of RAM — more than the entire server. Twenty-three other sites were set to 60 workers each.
The default pm.max_requests value of 0 means workers never recycle. Each worker runs indefinitely, slowly accumulating memory from WordPress plugin leaks, until the server crashes. This is the most common cause of the "RAM gradually fills up over 4–8 hours" pattern.
The Fix — Bulk Update All Domain Configs
These four commands update every single domain on your server at once:
# Cap maximum workers per site
sed -i 's/^pm.max_children = .*/pm.max_children = 20/' /opt/plesk/php/8.3/etc/php-fpm.d/*.conf
# Force worker recycling after 100 requests (prevents memory bloat)
sed -i 's/^pm.max_requests = .*/pm.max_requests = 100/' /opt/plesk/php/8.3/etc/php-fpm.d/*.conf
# Switch to ondemand - workers spawn when needed, die when idle
sed -i 's/^pm = .*/pm = ondemand/' /opt/plesk/php/8.3/etc/php-fpm.d/*.conf
# Restart PHP-FPM
systemctl restart plesk-php83-fpm
dynamic: Keeps min_spare_servers alive permanently, even with zero traffic. On a 37-site server, this means hundreds of idle workers consuming RAM 24/7.
ondemand: Workers only exist when there are active requests. They spawn in milliseconds and die when idle. For most shared hosting scenarios, this is the correct mode.
After switching to ondemand and restarting, RAM usage dropped from 5.8GB to just 1.8GB instantaneously. Workers that had been sitting idle for hours were finally released.
| Setting | Before | After | Effect |
|---|---|---|---|
| pm | dynamic | ondemand | Workers die when idle |
| pm.max_children | 60–120 per site | 20 | Prevents RAM exhaustion |
| pm.max_requests | 0 (never recycle) | 100 | Stops memory leak accumulation |
| Active workers | 278 | ~10–20 | Massive RAM saving |
5. OPcache Configuration
OPcache pre-compiles PHP scripts and stores them in memory, eliminating the need to parse and compile the same files thousands of times per day. It's one of the highest-impact, lowest-effort optimisations you can make.
In Plesk, OPcache settings are configured at Tools & Settings → PHP Settings → PHP 8.3 → Additional configuration directives. Here's the optimised config:
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.validate_timestamps=0
The key setting here is opcache.validate_timestamps=0. When set to 1 (default), PHP checks the filesystem on every request to see if any file has changed — this is thousands of unnecessary disk reads per second on a busy server. Setting it to 0 tells OPcache to trust its cache completely, which dramatically improves response times.
With timestamps disabled, PHP won't notice when you update plugin or theme files. After any code changes, you'll need to either restart PHP-FPM (systemctl restart plesk-php83-fpm) or use a plugin like Nginx Helper to clear OPcache automatically on updates. Don't use this on sites under active development.
Note that individual domain PHP settings in Plesk override global settings. If you've configured PHP per-domain, you'll need to add OPcache directives to each domain's additional directives section as well — or edit the conf files directly. See the official OPcache documentation for the full list of available options.
6. Installing & Configuring Redis for WordPress
Redis is an in-memory data store that WordPress uses to cache database queries. Without it, WordPress hits MariaDB on every single page load for hundreds of queries. With it, most requests are served directly from memory in microseconds.
Redis isn't available as a Plesk component, but it installs cleanly alongside Plesk via apt without any conflicts:
# Install Redis and the PHP extension
apt install redis-server php8.3-redis -y
# Enable on boot
systemctl enable redis-server
systemctl start redis-server
# Verify it's running
redis-cli ping
# Should return: PONG
# Verify PHP can see Redis
php -m | grep redis
Set a Memory Limit
Without a memory limit, Redis will grow indefinitely. Edit /etc/redis/redis.conf and set:
maxmemory 512mb
maxmemory-policy allkeys-lru
systemctl restart redis-server
allkeys-lru tells Redis to evict the least-recently-used keys when it hits the memory limit — perfect for a WordPress object cache where stale data should be the first to go.
Connecting WordPress to Redis
Install the Redis Object Cache plugin on each WordPress site:
- Go to Plugins → Add New → search "Redis Object Cache"
- Install and activate
- Go to Settings → Redis
- Click Enable Object Cache
That's it. WordPress will immediately start serving cached database queries from Redis instead of MariaDB. To verify it's working:
redis-cli info stats | grep keyspace_hits
redis-cli info keyspace
On my server, within a few hours of enabling Redis across my WordPress sites, the hit count reached 461,000+. That's nearly half a million database queries served from memory instead of hitting MariaDB. The impact on page load times is immediate and measurable.
Redis + WP Rocket
Redis Object Cache and WP Rocket work together perfectly — they do different jobs. WP Rocket handles page caching, CSS/JS minification, and lazy loading. Redis handles the WordPress object cache underneath. The one thing to check: disable WP Rocket's own object cache feature (if enabled) so Redis handles that layer instead.
7. Cloudflare WAF Rules — Bot Blocking Without Breaking AdSense
Bots were consuming significant server resources — crawling wp-admin directories, hammering xmlrpc.php, running directory traversal scans. The challenge is blocking the bad actors without accidentally blocking Google's ad verification crawlers and killing your AdSense revenue.
I learned this the hard way: enabling Cloudflare's Bot Fight Mode did reduce bot traffic, but it also reduced AdSense impressions and legitimate traffic metrics. Ad networks use their own crawlers to verify ad placement and traffic quality — and Bot Fight Mode is too aggressive to distinguish between a malicious scanner and an AdSense verification bot.
The solution is custom WAF rules that target specific attack vectors without touching ad network infrastructure. On Cloudflare's free plan you get 5 custom rules — here's how to use them effectively. Go to Security → WAF → Custom Rules and use the "Edit expression" link to paste each rule directly:
Rule 1 — Block Malicious Scanners
Rule 2 — Protect wp-admin
Using Interactive Challenge rather than Block means legitimate users (including yourself if your IP changes) will see a one-click verification rather than a hard block. The admin-ajax.php exception is critical — WooCommerce, contact forms, and many plugins rely on this endpoint.
Rule 3 — Challenge wp-login
Rule 4 — Block Sensitive Files
Rule 5 — Reserve for Site-Specific Needs
Keep Rule 5 free for anything that comes up — blocking a specific country hammering one of your sites, rate-limiting a particular endpoint, or responding to a new attack pattern you spot in the logs.
Google's ad crawlers identify themselves as AdsBot-Google, Googlebot, and Mediapartners-Google. None of these rules block based on user agent. They only block specific URI patterns that no legitimate ad crawler or search engine bot would ever request. Your impressions, CTR, and RPM remain completely unaffected.
8. Plesk Security & Nginx Bot Blocking
Cloudflare handles traffic before it reaches your server. But adding server-level protection means even direct-to-IP attacks — which bypass Cloudflare entirely — are blocked at the nginx layer before PHP ever loads.
Per-Domain nginx Restrictions in Plesk
For any WordPress site, go to Plesk → Domain → Apache & nginx Settings → Additional nginx directives and add:
location ~* ^/wp-admin/(?!admin-ajax\.php) {
allow YOUR.IP.ADDRESS.HERE;
deny all;
}
location = /wp-login.php {
allow YOUR.IP.ADDRESS.HERE;
deny all;
}
location = /xmlrpc.php {
deny all;
}
Replace YOUR.IP.ADDRESS.HERE with your IP. Find it with:
echo $SSH_CLIENT | awk '{print $1}'
This blocks the entire world from accessing wp-admin and wp-login at the nginx level — no PHP process is ever spawned for these blocked requests, which means zero CPU and memory overhead from bot scanning.
Safe nginx Bot Blocking
You can also create a global nginx config to block known malicious scanners. Create /etc/nginx/conf.d/block-bots.conf:
map $http_user_agent $blocked_agent {
default 0;
~*masscan 1;
~*zgrab 1;
~*nikto 1;
~*sqlmap 1;
~*havij 1;
~*acunetix 1;
~*nessus 1;
}
Many legitimate services — payment gateways, Google services, monitoring tools — identify as curl or python-requests. Only block user agents that belong exclusively to known attack tools. Googlebot, Bingbot, and AdsBot-Google are completely safe — they use their own distinct identifiers that nothing in this list will touch.
nginx -t && systemctl reload nginx
What Was Actually Hitting My Sites
Looking at the downloadautographs.com error log, all the bot traffic was coming from Cloudflare IP ranges (172.x.x.x, 162.158.x.x) — meaning the site was behind Cloudflare, but the bots were somehow getting through undetected. Several requests even had referer: binance.com — a classic spoofed referrer used by scrapers to disguise their origin. This is exactly the kind of traffic that the WAF rules above are designed to catch.
9. Final Results & Summary
Here's where we ended up after a full day of optimisation work — a completely transformed server that went from crashing nine times in two days to running stable with enormous headroom.
❌ Before
- 19.9GB RAM used (crashing)
- 9 server crashes in 2 days
- Zero swap space
- 12GB MariaDB buffer (11.5GB wasted)
- 278 PHP-FPM workers
- Workers never recycled (pm.max_requests=0)
- Bots consuming CPU uncontrolled
- Zero Redis caching
- Backups failing completely
✅ After
- 3.8GB RAM used (rock stable)
- Zero crashes
- 4GB swap safety net
- 2GB MariaDB buffer (right-sized)
- ~10–20 PHP-FPM workers
- Workers recycle every 100 requests
- Bots blocked at Cloudflare + nginx
- 461,000+ Redis cache hits
- Backups running cleanly
The Complete Checklist
Diagnose with journalctl and dmesg
Never guess. journalctl -p err..crit and dmesg | grep -i oom will tell you exactly what's failing and when.
Add swap immediately
4GB swap + vm.swappiness=10. Protects against spikes from scanners, cron jobs, and traffic bursts.
Find and fix the real MariaDB buffer pool
Check /etc/mysql/my.cnf — Plesk may have set it to 8–12GB. Reduce to 2GB for most shared VPS setups.
Bulk update PHP-FPM configs
Switch to ondemand, set pm.max_children=20, and critically, set pm.max_requests=100 to stop memory leaks accumulating.
Install Redis and connect WordPress
Install via apt, set a 512MB memory limit, install Redis Object Cache plugin on every WordPress site.
Configure OPcache correctly
128MB, 10,000 files, validate_timestamps=0 for production sites. Stops PHP parsing the same files thousands of times per day.
Set up Cloudflare WAF rules
4 targeted rules covering scanners, wp-admin, wp-login, and sensitive files. No Bot Fight Mode — it breaks AdSense.
Add nginx-level protection
Whitelist your IP for wp-admin and wp-login. Block xmlrpc.php entirely. Blocks happen before PHP loads — zero resource cost.
Monitoring Commands to Keep Handy
# RAM and swap at a glance
free -h
# PHP worker count
ps aux | grep "php-fpm: pool" | wc -l
# Per-site worker breakdown
ps aux | grep "php-fpm: pool" | awk '{print $NF}' | sort | uniq -c | sort -rn
# Redis hit rate
redis-cli info stats | grep keyspace_hits
redis-cli info keyspace
# Watch RAM and worker count together every 5 seconds
watch -n 5 'free -h && echo "---" && ps aux | grep "php-fpm: pool" | wc -l'
# Check for OOM kills
dmesg | grep -i "oom\|killed process" | tail -20
# Slow query log
tail -50 /var/log/mysql/slow.log
Further Reading
- MariaDB InnoDB System Variables — Official Documentation
- PHP-FPM Configuration — php.net
- Redis Key Eviction Policies — redis.io
- Cloudflare Custom WAF Rules Documentation
- Redis Object Cache Plugin — WordPress.org
- How to Secure Ubuntu — Complete Server Hardening Guide
Need Help With Your Server?
I offer WordPress speed optimisation, security hardening, and Plesk VPS management as a service. Every client gets lifetime support — no expiring warranty.
View Speed Optimisation Service




