Most Magento 2 performance work stops at the application layer: PHP-FPM pools, Nginx config, MySQL buffers, Redis, Varnish. That's where the visible wins are. But underneath all of it sits the operating system, and on a stock Ubuntu or Debian install it's running kernel, filesystem and I/O defaults that were never designed for an application that reads and writes thousands of small files and pounds a database with mixed random I/O every second.
This guide covers the OS-level settings that actually move the needle for Magento 2: filesystem mount options, I/O scheduler, memory tuning (swappiness, dirty pages, Transparent Huge Pages), CPU governor, file descriptor limits and the tuned profile. Everything is verifiable, and most changes are two lines in sysctl.conf plus a remount.
Why the OS layer matters for Magento
Magento 2's workload profile is specific:
-
Thousands of small files.
pub/static,var/view_preprocessed,generated/andpub/mediaadd up to hundreds of thousands of inodes. Every static content deploy rewrites tens of thousands of them. -
Mixed random I/O on MySQL. The
catalog_product_index_pricerebuild, order grid indexing and log cleanup all do heavy sequential-ish scans, while checkout does small random reads. Your storage handles both at once. - Bursty cache writes. Redis writes, session files (if filesystem-backed), Varnish cache — all bursty, all latency-sensitive.
- Page cache dependency. A single framework-level cache miss cascades into dozens of MySQL round-trips. The OS can't fix that, but it decides how fast those disk-backed misses resolve.
If your stack is already well-tuned and your bottleneck is still I/O or latency spikes under load, the OS layer is the next place to look.
1. Filesystem: ext4 vs XFS and mount options
The filesystem choice matters less than most people think — modern ext4 and XFS both handle Magento workloads fine. What matters more is how they're mounted.
Minimal mount options that are safe on ext4 for a Magento server:
/dev/sdb1 /var/www ext4 defaults,noatime,nodiratime,errors=remount-ro 0 2
-
noatime— the big one. Every file read updates the atime (access time) by default, which means a metadata write on every read. Magento reads the same static files and opcache-scanned PHP files thousands of times a day. Disabling atime eliminates a whole class of unnecessary writes.nodiratimeis redundant on modern kernels whennoatimeis set, but harmless to include. -
commit=60(ext4) — the journal commit interval. Default is 5 seconds; raising it to 30–60 reduces journal write pressure on busy data volumes. Only for non-critical data partitions; don't do this on a volume whose recent writes you'd hate to lose. -
discardvsfstrim— on SSDs/NVMe, periodicfstrim(via cron or systemd timer) is safer than thediscardmount option, which can cause I/O stalls on some controllers. Usefstrim -avweekly.
Verify:
mount | grep -E 'var/www|pub'
# "noatime" should be in the options list
A note on var/ and generated/: some teams put var/cache, var/session or generated/ on tmpfs (RAM disk) to eliminate disk I/O entirely. This works and is fast, but it's volatile: a reboot empties them, so var/cache and generated/ must be rebuildable in your deploy (they are — cache is warmable, generated code comes from bin/magento setup:di:compile). Never put anything irreplaceable there, and size it carefully (a generated/ dir can exceed 1–2 GB on big catalogs). For most setups, a fast NVMe with noatime is the safer 90% of the win.
2. I/O scheduler: know what you're running on
The I/O scheduler decides how requests are queued to the disk. Since kernel 5.x on NVMe, the default is none (noop), which is correct — NVMe devices with deep hardware queues don't need software scheduling. On SATA SSDs and HDDs, mq-deadline is the right choice.
Check with:
cat /sys/block/sda/queue/scheduler
If it shows [none] on a SATA SSD or HDD, switch to mq-deadline at boot via a udev rule:
# /etc/udev/rules.d/60-iosched.rules
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/scheduler}="mq-deadline"
What really matters for Magento is not letting the scheduler starve your database. If MySQL, Redis and the web server share one disk, heavy batch jobs (reindex, log cleanup, static content deploy) can saturate the queue and push checkout latency through the roof. Two mitigations:
- Run batch jobs with
ionice:ionice -c 2 -n 7 bin/magento indexer:reindex— best-effort class, lowest priority, so the queue still services foreground reads. - If you can, split storage: database on one volume,
pub/media+var/on another. This is the single most effective I/O isolation you can do on a single server.
3. Memory: swappiness, dirty pages and THP
Three settings that are consistently wrong on stock installs for Magento + MySQL servers.
vm.swappiness — controls how eagerly the kernel swaps anonymous memory. Default is 60, which is far too aggressive for a server with a large page cache. For database servers, vm.swappiness = 10 is the classic recommendation; for Magento with Redis caching, 10 is a good middle ground (Redis' own maxmemory policy handles eviction, and you don't want Redis pages swapped out). On systems with plenty of RAM, 0–10.
# /etc/sysctl.d/99-magento.conf
vm.swappiness = 10
Dirty page writeback — when the kernel accumulates too many dirty pages, it flushes them in bursts, causing I/O stalls that show up as MySQL latency spikes. A known-good pairing for DB servers:
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
dirty_background_ratio starts background writeback at 5% dirty memory, dirty_ratio forces synchronous flush at 15%. The defaults (10/20 on many distros) let dirtiness climb higher before acting, producing bigger, nastier bursts. On a server with 64 GB RAM these numbers mean MySQL's double-write buffer and redo log flushes stay smooth instead of queuing behind a wall of page-cache writes.
Transparent Huge Pages (THP) — the single most famous OS-level fix in the MySQL world. THP is enabled by default (always), and while it helps some workloads, for MySQL it's documented to cause severe latency issues: memory is allocated in 2 MB huge pages, and during defragmentation the kernel stalls threads waiting for page compaction. Every major distro's MySQL docs say the same thing: set it to madvise or never.
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
To make it persistent:
# /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled && echo never > /sys/kernel/mm/transparent_hugepage/defrag'
[Install]
WantedBy=multi-user.target
Verify: cat /sys/kernel/mm/transparent_hugepage/enabled should show [never] or [madvise]. This alone has fixed "random" MySQL latency spikes on more Magento servers than I can count.
4. CPU governor: stop letting the kernel downclock your store
On bare metal and VPS with exposed CPU frequency scaling, the default powersave/ondemand governor downclocks cores aggressively, which shows up as slow PHP execution — exactly the symptom people blame on "Magento being slow." For a server, set it to performance:
cpupower frequency-set -g performance # or:
echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
Persist via tuned (below) or cpupower.service. Note: on most cloud VPS (AWS, DigitalOcean, Hetzner) the governor is not exposed or already pinned — check cpupower frequency-info and skip this if it's not settable.
5. File descriptor limits: the silent 1024-cap
PHP-FPM, Nginx, Elasticsearch/OpenSearch, Redis and MySQL all need more than the default 1024 soft file descriptor limit when the store is busy. Symptoms of hitting it: "Too many open files" in php-fpm.log, MySQL Can't open file errors, ES red logs for no obvious reason.
Raise the system limit and per-service limits:
# /etc/sysctl.d/99-magento.conf
fs.file-max = 2097152
And in each systemd unit (PHP-FPM, Elasticsearch, Redis, MySQL), add:
[Service]
LimitNOFILE=65535
This is one of those settings that never shows up in app-level profiling but is a classic cause of "random" errors at peak traffic. If you see these errors only during Black Friday / flash-sale spikes, check this first.
6. Just use tuned: throughput-performance
Instead of hand-rolling all of this per distro flavor, install tuned and apply the throughput-performance profile, which sets the sane defaults for servers (governor to performance, swappiness to 10, THP to madvise, noatime-ish behavior):
apt install tuned
tuned-adm profile throughput-performance
tuned-adm active # verify
You still want to explicitly set vm.dirty_* and disable THP for MySQL yourself (the profile only sets THP to madvise; many DBAs prefer never), but tuned gives you a solid, persistent baseline in one command.
7. What to measure after applying
Every change here should be justified by a before/after. The tools that matter:
-
iostat -x 2— look at%util,await,svctm. Ifawaitis high on MySQL's volume during reindex, your I/O layer is the bottleneck, not the query. -
vmstat 2—si/socolumn (swap in/out) should be near zero. If you see steady swapping, you're undersized on RAM or swappiness is wrong. -
MySQL
SHOW GLOBAL STATUSforInnodb_buffer_pool_wait_free— if this counter climbs, InnoDB is waiting for page flushes under memory pressure; dirty-page tuning and buffer pool sizing are your lever. -
dmesg/journalctl -kfor "stall" or THP compaction messages — smoking gun for THP-related latency.
Apply changes one at a time, keep the sysctl file small and commented, and re-run your load test (k6, siege, or a replay of your slow-query log) after each change.
The checklist
Here's the full set, condensed — safe on any Ubuntu/Debian Magento 2 server:
# /etc/sysctl.d/99-magento.conf
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
fs.file-max = 2097152
- Remount data partitions with
noatime(verify withmount). - I/O scheduler:
noneon NVMe,mq-deadlineon SATA/HDD. - THP →
never, persisted via systemd unit. -
tuned-adm profile throughput-performance(+ CPU governor toperformancewhere exposed). -
LimitNOFILE=65535on PHP-FPM, ES, Redis, MySQL systemd units. -
ioniceyour reindex/cron batch jobs; separate MySQL from media/var volumes if possible.
Skip-worthy: swappiness below 10 (no benefit, hurts page cache), noatime on commit=0 journal tricks, aggressive vm.dirty_expire_centisecs micro-tuning, and any kernel compile-level "optimizations" you found in a 2015 blog post. The list above is the tested 90%.
The honest bottom line
The OS layer is where the last 5–10% of a Magento 2 server's performance lives — but only after the app layer is done. If your MySQL slow-query log is full, your FPM pm.max_children is wrong, or your Varnish hit rate is under 90%, fix those first. When you've done that and the server still feels "laggy under load" without an obvious app-level cause, the settings above — especially THP, swappiness/dirty ratios and I/O scheduler — are where the hidden latency actually lives. They're free, they're reversible, and unlike most Magento "performance tips," they're verifiable with three commands.
Top comments (0)