The First Rule: Identify the Bottleneck
The biggest mistake in VPS optimization is tuning things that are not the problem. Before you change a single setting, identify what is actually slow. The bottleneck is always one of four things: CPU, memory, storage I/O, or network. Optimizing the wrong one wastes your time and can make things worse.
Start with these diagnostic commands on your Ubuntu VPS:
# Overall system load and resource usage
top -bn1 | head -20
# Disk I/O statistics
iostat -xz 1 5
# Memory usage breakdown
free -h
vmstat 1 5
# Network connections and throughput
ss -s
sar -n DEV 1 5
# Which processes are using the most resources
ps aux --sort=-%mem | head -15
ps aux --sort=-%cpu | head -15
Spend 10 minutes watching these numbers under load before you touch any configuration files. The data will tell you exactly where to focus.
CPU Optimization
CPU bottlenecks show up as high load averages (consistently above your vCPU count), processes in a "waiting" state, and slow application response times even when memory and disk are fine.
Process Priority with Nice Levels
If background tasks are competing with your web server, use nice levels to prioritize:
# Run backup jobs at lower priority
nice -n 19 tar -czf /backups/site.tar.gz /var/www/
# Give your web server higher priority (requires root)
renice -5 -p $(pgrep nginx)
# Set nice levels in systemd service files
# In /etc/systemd/system/myapp.service:
# [Service]
# Nice=-5
CPU Governor and Scheduler
On a VPS, the CPU governor is typically managed by the hypervisor, but you can tune the process scheduler:
# Check current scheduler for a disk (useful for I/O-heavy workloads)
cat /sys/block/sda/queue/scheduler
# Set the kernel scheduler time slice for better interactive performance
echo 'kernel.sched_min_granularity_ns = 10000000' >> /etc/sysctl.d/99-performance.conf
echo 'kernel.sched_wakeup_granularity_ns = 15000000' >> /etc/sysctl.d/99-performance.conf
sysctl --system
If you consistently need more CPU than your current plan provides, the right answer is scaling up, not squeezing more out of a constrained environment. On MassiveGRID, you can independently add vCPU cores without changing your RAM or storage allocation.
Memory Optimization
Memory problems manifest as high swap usage, the OOM (Out of Memory) killer terminating processes, or gradual performance degradation as the system struggles to cache effectively.
Swap and Swappiness
Ubuntu defaults to a swappiness of 60, which is too aggressive for a server. Lower it to keep more data in RAM:
# Check current swappiness
cat /proc/sys/vm/swappiness
# Set swappiness to 10 (prefer RAM, use swap only when necessary)
echo 'vm.swappiness = 10' >> /etc/sysctl.d/99-performance.conf
# Tune VFS cache pressure (lower = keep directory/inode caches longer)
echo 'vm.vfs_cache_pressure = 50' >> /etc/sysctl.d/99-performance.conf
# Apply immediately
sysctl -p /etc/sysctl.d/99-performance.conf
OOM Killer Configuration
Protect critical processes from the OOM killer:
# Make MySQL less likely to be killed (-1000 to 1000, lower = more protected)
echo -1000 > /proc/$(pgrep -x mysqld)/oom_score_adj
# For a permanent solution, add to the systemd service:
# [Service]
# OOMScoreAdjust=-1000
# Check which process the OOM killer would target first
printf '%s\t%s\t%s\n' "PID" "OOM Score" "Command"; \
for pid in /proc/[0-9]*; do
p=$(basename "$pid")
[ -r "$pid/oom_score" ] && printf '%s\t%s\t%s\n' \
"$p" "$(cat $pid/oom_score)" "$(cat $pid/comm 2>/dev/null)"
done | sort -t$'\t' -k2 -rn | head -10
Transparent Huge Pages
For database servers, disabling Transparent Huge Pages (THP) can reduce latency spikes:
# Check current THP status
cat /sys/kernel/mm/transparent_hugepage/enabled
# Disable THP (recommended for MySQL, MongoDB, Redis)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# Make persistent via /etc/rc.local or systemd unit
Network Optimization
Network tuning has some of the highest return on investment, especially for web servers handling many concurrent connections.
TCP BBR Congestion Control
Google's BBR algorithm dramatically improves throughput and reduces latency compared to the default CUBIC:
# Enable BBR
echo 'net.core.default_qdisc = fq' >> /etc/sysctl.d/99-network.conf
echo 'net.ipv4.tcp_congestion_control = bbr' >> /etc/sysctl.d/99-network.conf
# Verify BBR is available
sysctl net.ipv4.tcp_available_congestion_control
Buffer Sizes and Connection Tuning
Create a comprehensive network tuning configuration:
# /etc/sysctl.d/99-network.conf
# TCP BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# Increase buffer sizes for high-bandwidth connections
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Connection handling
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
# TIME_WAIT optimization
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# Keepalive (detect dead connections faster)
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
# Connection tracking (important for firewalled servers)
net.netfilter.nf_conntrack_max = 262144
# Enable TCP Fast Open
net.ipv4.tcp_fastopen = 3
# Apply all settings
sysctl --system
Storage I/O Optimization
MassiveGRID uses Ceph 3x replicated NVMe storage, which is already fast. But your OS configuration can add unnecessary overhead.
Filesystem Mount Options
Add noatime to your mount options to eliminate unnecessary write operations:
# Edit /etc/fstab - add noatime to your root partition
# Before: UUID=xxx / ext4 defaults 0 1
# After: UUID=xxx / ext4 defaults,noatime 0 1
# Apply without reboot
mount -o remount,noatime /
I/O Scheduler
For NVMe-backed storage (like MassiveGRID's Ceph), the none (noop) scheduler is optimal:
# Check current scheduler
cat /sys/block/sda/queue/scheduler
# Set to none for NVMe-backed virtual disks
echo none > /sys/block/sda/queue/scheduler
# Adjust readahead for sequential workloads
blockdev --setra 2048 /dev/sda
Application Caching
The fastest request is one that never hits your application. Caching at multiple layers has the biggest impact on perceived performance.
Redis for Application Caching
# Install Redis
apt update && apt install -y redis-server
# Configure Redis for caching (not persistence)
# /etc/redis/redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru
save ""
appendonly no
# Restart Redis
systemctl restart redis-server
systemctl enable redis-server
# Verify
redis-cli ping
PHP OPcache
If you are running PHP, OPcache eliminates the overhead of parsing PHP files on every request:
# /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.validate_timestamps=1
opcache.save_comments=1
opcache.fast_shutdown=1
# Restart PHP-FPM
systemctl restart php8.3-fpm
Nginx FastCGI Cache
For PHP applications behind Nginx, FastCGI caching can serve cached pages directly from Nginx without touching PHP at all:
# In nginx.conf (http block)
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# In your server block
set $skip_cache 0;
# Don't cache POST requests
if ($request_method = POST) { set $skip_cache 1; }
# Don't cache URLs with query strings
if ($query_string != "") { set $skip_cache 1; }
# Don't cache logged-in users (WordPress example)
if ($http_cookie ~* "wordpress_logged_in") { set $skip_cache 1; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 60m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache-Status $upstream_cache_status;
}
Web Server Tuning (Nginx)
Nginx's default configuration is conservative. Tune it for your VPS resources:
# /etc/nginx/nginx.conf
# Set to number of CPU cores (or auto)
worker_processes auto;
# Maximum connections per worker
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
# Buffers
client_body_buffer_size 16k;
client_header_buffer_size 1k;
client_max_body_size 64m;
large_client_header_buffers 4 8k;
# Timeouts
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 65;
send_timeout 10;
keepalive_requests 1000;
# Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript
application/vnd.ms-fontobject application/x-font-ttf
font/opentype image/svg+xml image/x-icon;
# File caching
open_file_cache max=10000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# TCP optimization
sendfile on;
tcp_nopush on;
tcp_nodelay on;
}
# Test configuration and reload
nginx -t && systemctl reload nginx
MySQL/MariaDB Tuning
Database performance is the most common bottleneck for web applications. These settings should be adjusted based on your available RAM:
# /etc/mysql/mysql.conf.d/mysqld.cnf (MySQL)
# or /etc/mysql/mariadb.conf.d/50-server.cnf (MariaDB)
[mysqld]
# InnoDB buffer pool - set to 50-70% of available RAM
# For a 4GB VPS, use 2G. For 8GB, use 4-5G.
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
# Log file size (larger = better write performance, slower crash recovery)
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M
# I/O threads
innodb_read_io_threads = 4
innodb_write_io_threads = 4
# Flush behavior (2 = flush once per second, good balance)
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# Connection handling
max_connections = 200
thread_cache_size = 16
# Temporary tables
tmp_table_size = 64M
max_heap_table_size = 64M
# Query optimizations
join_buffer_size = 4M
sort_buffer_size = 4M
read_rnd_buffer_size = 2M
# Slow query log (essential for finding problems)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
# Restart MySQL to apply
systemctl restart mysql
# After running for a few hours, check buffer pool utilization
mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';"
mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_reads';"
# Hit ratio should be above 99%
Slow Query Analysis
# Install and run pt-query-digest for slow query analysis
apt install percona-toolkit
pt-query-digest /var/log/mysql/slow.log | head -100
# Or use mysqldumpslow for a quick overview
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
Monitoring with Netdata
After making optimizations, you need to measure their impact. Netdata provides real-time monitoring with minimal overhead:
# One-line installation
curl https://get.netdata.cloud/kickstart.sh > /tmp/netdata-kickstart.sh && \
sh /tmp/netdata-kickstart.sh --stable-channel
# Access dashboard at http://your-server-ip:19999
# Key dashboards to watch:
# - System Overview: CPU, RAM, load
# - Disk I/O: throughput, latency, utilization
# - Network: bandwidth, errors, drops
# - Applications: per-process resource usage
# - MySQL/Nginx: if plugins are detected
Watch your metrics for 24–48 hours after making changes. Compare against your baseline to verify improvements.
When to Scale vs When to Optimize
Optimization has diminishing returns. At some point, you have squeezed everything possible out of your current resources and the answer is more resources, not more tuning.
Signs you need to scale, not optimize:
- CPU is consistently above 80% during normal traffic
- RAM usage is above 90% and swap is active
- You have already implemented caching and the application is still slow
- Response times increase linearly with traffic
On MassiveGRID, scaling is independent: you can add CPU without changing RAM, or add storage without touching anything else. This means you scale exactly what is constrained, nothing more. Cloud VPS lets you adjust resources on demand, while Cloud VDS provides dedicated resources for predictable performance under load.
The Noisy Neighbor Problem
If your performance is inconsistent—fast at 3 AM, slow at noon, with no change in your traffic—the bottleneck might not be your configuration. It might be another tenant on the same host node consuming shared resources.
This is the "noisy neighbor" problem, and no amount of tuning on your end will fix it. The solution is dedicated resources with a Cloud VDS, where your CPU and RAM are exclusively yours. Same self-managed control, same infrastructure, but guaranteed performance regardless of what other tenants are doing.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
→ Deploy a self-managed VPS — from $1.99/mo
→ Need dedicated resources? — from $8.30/mo
→ Want fully managed hosting? — we handle everything
Complete Optimization Checklist
Here is a summary checklist you can work through systematically. Do not apply everything at once—make one change at a time, measure the impact, then move on.
Kernel and System Level
- Set
vm.swappiness = 10 - Set
vm.vfs_cache_pressure = 50 - Enable TCP BBR congestion control
- Increase network buffer sizes and connection limits
- Add
noatimeto filesystem mount options - Disable Transparent Huge Pages for database servers
- Enable TCP Fast Open
Web Server (Nginx)
- Set
worker_processes auto - Increase
worker_connectionsto 4096+ - Enable gzip compression with appropriate types
- Configure
open_file_cache - Enable
sendfile,tcp_nopush,tcp_nodelay - Set up FastCGI caching for PHP applications
- Configure keepalive with appropriate timeouts
Database (MySQL/MariaDB)
- Set
innodb_buffer_pool_sizeto 50–70% of RAM - Enable slow query logging
- Set
innodb_flush_log_at_trx_commit = 2(if you can accept minimal data loss risk) - Use
innodb_flush_method = O_DIRECT - Review and optimize slow queries with
pt-query-digest - Ensure proper indexing on frequently queried columns
Application Layer
- Install and configure Redis for object and session caching
- Enable PHP OPcache with appropriate memory limits
- Configure your application to use Redis for caching
- Enable HTTP/2 in your web server configuration
- Set appropriate cache headers for static assets
Benchmarking Your Changes
Before and after each optimization, run a consistent benchmark so you can quantify the improvement:
# Simple HTTP benchmark with Apache Bench
ab -n 1000 -c 50 https://yourdomain.com/
# More detailed testing with wrk
apt install wrk
wrk -t4 -c100 -d30s https://yourdomain.com/
# Check Time to First Byte from external locations
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://yourdomain.com/
Record results in a spreadsheet. Track response time (p50, p95, p99), requests per second, and error rate. This data is invaluable when deciding whether to optimize further or scale up.
When Optimization Is Not Enough
You have applied all the tuning above, your cache hit rates are above 90%, your slow query log is clean, and your application code is efficient. But traffic keeps growing and response times are creeping up.
At this point, you have two options:
- Vertical scaling: Add more CPU, RAM, or storage to your existing server. On MassiveGRID, this is independent—add just the resource you need.
- Horizontal scaling: Add more servers (load balancer + multiple application servers + separate database server). This is more complex but provides both more capacity and redundancy.
If you are running a Cloud VPS and performance is inconsistent despite optimization, the issue may be shared resources. Upgrading to a Cloud VDS with dedicated CPU and RAM eliminates resource contention entirely.
And if managing all of this optimization and scaling has become a full-time job you did not sign up for, Managed Cloud Dedicated Servers handle all tuning, monitoring, and scaling decisions for you.