Storage I/O is the most common bottleneck for database workloads on a VPS. CPU and RAM are abundant on modern virtual servers, but the virtualized storage layer often introduces latency and throughput limits. Tuning the filesystem mount options, I/O scheduler, and kernel parameters that control write behavior can improve database throughput by 20–50% on the same hardware. This article covers the tuning options that matter for MySQL, PostgreSQL, and Redis running on a VPS.
Understanding Virtualized Storage on a VPS
Unlike bare-metal servers where you control the physical disk, a VPS has a virtual block device backed by the hypervisor’s storage subsystem. The hypervisor may be running on NVMe SSDs, but the virtualization layer adds latency and can introduce I/O contention from neighboring VPS instances. The tuning parameters you control are at the guest OS level — the filesystem, mount options, and I/O scheduler.
| Storage Type | Typical Latency | Typical IOPS | Throughput | Best For |
|---|---|---|---|---|
| HDD-backed VPS | 5–10 ms | 100–500 | 50–150 MB/s | Archival, low-traffic |
| SSD-backed VPS | 0.5–2 ms | 3,000–10,000 | 200–500 MB/s | Most workloads |
| NVMe-backed VPS | 0.1–0.5 ms | 10,000–100,000 | 500–3,000 MB/s | Databases, high-traffic |
Measure Your Baseline I/O Performance
Before tuning, measure your current I/O performance with fio (Flexible I/O Tester):
# Install fio
sudo apt install fio -y
# Random read IOPS (simulates database index lookups)
fio --name=random-read \
--ioengine=libaio --direct=1 --bs=4k \
--rw=randread --numjobs=4 --runtime=30 \
--time_based --group_reporting \
--filename=/tmp/fio-test
# Random write IOPS (simulates database writes)
fio --name=random-write \
--ioengine=libaio --direct=1 --bs=4k \
--rw=randwrite --numjobs=4 --runtime=30 \
--time_based --group_reporting \
--filename=/tmp/fio-test
# Sequential throughput (simulates backups and large queries)
fio --name=sequential-read \
--ioengine=libaio --direct=1 --bs=1M \
--rw=read --numjobs=1 --runtime=30 \
--time_based --group_reporting \
--filename=/tmp/fio-test
rm /tmp/fio-test
Record your baseline IOPS and throughput. After tuning, rerun these benchmarks to measure the improvement.
Filesystem Mount Options
The mount options you pass to the filesystem driver control how data is written to disk. For database workloads, the default options are suboptimal. Here are the recommended mount options for ext4 (the most common VPS filesystem):
| Option | Effect | Database Impact |
|---|---|---|
noatime | Disables access time updates on files | Eliminates write operations on every file read. Reduces I/O by 10–15% for read-heavy workloads. |
nodiratime | Disables access time updates on directories | Reduced but less impactful than noatime (implied by noatime on modern kernels). |
discard | Enables TRIM/discard for SSD | Maintains SSD write performance over time. Use discard for continuous TRIM or set up a weekly fstrim cron job. |
data=writeback | Journal data mode (writeback) | Faster than ordered (default) but risks data corruption on crash. Only use if you have a UPS or are on a reliable VPS provider. |
barrier=0 | Disables write barriers | Faster but risks data loss on power failure. For database servers, keep barriers enabled (barrier=1) unless you use a battery-backed RAID controller. |
Apply mount options in /etc/fstab:
# /etc/fstab entry for database partition
UUID=your-uuid /var/lib/mysql ext4 defaults,noatime,discard 0 2
Remount to apply without rebooting:
sudo mount -o remount,noatime /var/lib/mysql
I/O Scheduler Selection
The I/O scheduler determines the order in which disk read/write requests are serviced. The kernel offers multiple schedulers, each optimized for different workloads:
| Scheduler | Best For | Latency | Throughput | Notes |
|---|---|---|---|---|
none (NVMe) | NVMe SSDs | Lowest | Highest | No scheduling overhead. NVMe devices have internal queues. |
mq-deadline | SSD-backed VPS | Low | High | Multi-queue deadline scheduler. Good for SSDs with moderate I/O. |
bfq | HDD or mixed workloads | Medium | Medium | Budget Fair Queueing. Good for interactive workloads on rotational disks. |
kyber | Latency-sensitive (SSD) | Low | High | Designed for fast devices. Good for databases on SSDs. |
Check the current scheduler and available options:
# Check current scheduler for your block device
cat /sys/block/sda/queue/scheduler
# Output: [mq-deadline] none
# List available schedulers (the one in brackets is active)
cat /sys/block/sda/queue/scheduler
# Output: [mq-deadline] kyber none
Change the scheduler (temporary, resets on reboot):
# For NVMe: use none
sudo bash -c 'echo none > /sys/block/nvme0n1/queue/scheduler'
# For SSD: use kyber or mq-deadline
sudo bash -c 'echo kyber > /sys/block/sda/queue/scheduler'
# For HDD: use bfq
sudo bash -c 'echo bfq > /sys/block/sda/queue/scheduler'
Make permanent by adding to GRUB (add elevator=kyber to GRUB_CMDLINE_LINUX in /etc/default/grub):
# /etc/default/grub
GRUB_CMDLINE_LINUX="elevator=kyber"
# Update GRUB and reboot
sudo update-grub
sudo reboot
Read-Ahead Tuning
The kernel reads ahead extra data from disk when it detects sequential access patterns. For databases, the default read-ahead value (usually 128 KB) is often too high for random-access workloads and too low for sequential scans. Tune it per block device:
# Check current read-ahead value
sudo blockdev --getra /dev/sda
# Output: 256 (sectors) = 128 KB
# For random-access database workloads (OLTP): reduce to 8–16 KB
sudo blockdev --setra 16 /dev/sda
# For sequential workloads (backups, analytics): increase to 512–2048 KB
sudo blockdev --setra 1024 /dev/sda
Make permanent with a udev rule at /etc/udev/rules.d/99-block-readahead.rules:
ACTION=="add|change", KERNEL=="sda", ATTR{bdi/read_ahead_kb}="8"
Database-Specific I/O Tuning
MySQL/MariaDB: InnoDB I/O Configuration
# /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
# Use O_DIRECT to avoid double buffering (page cache + buffer pool)
innodb_flush_method = O_DIRECT
# I/O capacity: set to your measured IOPS
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# Write I/O threads
innodb_write_io_threads = 4
innodb_read_io_threads = 4
# Adaptive flushing: adjust based on workload
# ON for SSD, OFF for HDD (to avoid excessive seeking)
innodb_adaptive_flushing = ON
# Use native AIO (asynchronous I/O) on Linux
innodb_use_native_aio = ON
PostgreSQL: WAL and Checkpoint Tuning
# postgresql.conf
# WAL configuration
wal_level = replica
max_wal_size = 2GB
min_wal_size = 256MB
# Checkpoint tuning (reduce I/O spikes)
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
# Effective I/O concurrency
# SSDs can handle higher concurrency
# HDD: 2, SSD: 200, NVMe: 1000
effective_io_concurrency = 200
# Random page cost (lower = planner prefers index scans)
# SSD: 1.1, NVMe: 1.0, HDD: 4.0
random_page_cost = 1.1
# Synchronous commit (off for performance, on for durability)
synchronous_commit = off
Redis: Persistence Configuration
# redis.conf
# AOF (Append-Only File) with fsync every second
appendonly yes
appendfsync everysec
# Disable RDB snapshots on low-memory VPS to avoid fork overhead
save ""
# Use AOF rewrite to compact the AOF file
# Redis automatically rewrites when AOF file grows 100% beyond last rewrite
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
Kernel Parameters for I/O
Add these to /etc/sysctl.d/99-vps-io.conf:
# /etc/sysctl.d/99-vps-io.conf
# Reduce dirty page ratio (flush data to disk more frequently)
# This prevents large I/O spikes during database checkpoints
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
# Dirty expire time (centiseconds)
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
# Swappiness (keep database pages in RAM)
vm.swappiness = 10
# Overcommit: allow applications to allocate memory (default for databases)
vm.overcommit_memory = 1
# VFS cache pressure (keep inode cache for database files)
vm.vfs_cache_pressure = 50
sudo sysctl -p /etc/sysctl.d/99-vps-io.conf
Benchmarking After Tuning
After applying all changes, rerun the fio benchmarks from Step 1. You should see:
- 15–25% improvement in random read IOPS (from
noatimeand scheduler tuning) - 10–20% improvement in random write IOPS (from dirty page tuning and
O_DIRECT) - Lower latency variance (from I/O scheduler selection)
If you do not see measurable improvement, the bottleneck is likely at the hypervisor level, not the guest OS. In that case, the only fix is to upgrade to a VPS plan with higher guaranteed IOPS or to an NVMe-backed plan.
When to Use a Separate Storage Volume
On VPS providers that offer block storage volumes (DigitalOcean Volumes, Linode Block Storage, AWS EBS), mounting a separate volume for database data isolates I/O from the OS disk. This prevents log rotation and apt updates from competing with database I/O. The same mount options and scheduler tuning apply to the block volume.
When selecting a VPS for your database workload, compare providers that offer NVMe storage and guaranteed IOPS — the difference between 3,000 IOPS (SSD) and 50,000 IOPS (NVMe) is the difference between a sluggish application and a responsive one. For more database performance guidance, explore our performance optimization guides.


Leave a Reply
You must be logged in to post a comment.