Skip to content

Linux Kernel & NVMe Storage Tuning: High-Throughput Database Optimization

Created: 2026-11-08  ·  Last updated: 2026-11-08  ·  Nejat Hakan

Executive Summary: Running an enterprise database on default Linux kernel parameters guarantees throughput bottlenecks and erratic latency spikes. Default distribution settings are tuned for general-purpose desktop or multi-tenant web workloads—not high-concurrency database engines. Unoptimized kernels aggressively swap database shared memory to disk, trigger multi-second I/O stalls during massive dirty page flushes, suffer Translation Lookaside Buffer (TLB) misses on 4 KiB memory pages, and assume storage consists of spinning mechanical hard drives. This guide details how to tune virtual memory sysctls, allocate 2 MiB Huge Pages, configure NVMe multi-queue I/O schedulers, and align PostgreSQL cost constants for modern PCIe storage.

  • Virtual Memory & Swappiness


    Prevent memory swapping: set vm.swappiness = 1 and configure explicit dirty memory thresholds (vm.dirty_background_ratio = 5) to eliminate write stalls.

  • 2 MiB Huge Pages


    Drastically reduce CPU TLB cache misses on multi-gigabyte buffer pools; disable erratic Transparent Huge Pages (THP) and pre-allocate static HugePages.

  • NVMe Multi-Queue Schedulers


    Bypass legacy single-queue kernel schedulers: assign none or mq-deadline to PCIe NVMe drives to unlock 500k+ parallel IOPS.

  • Planner Cost Calibration


    Update obsolete 1990s spinning-disk assumptions: align random_page_cost = 1.1 and effective_io_concurrency = 200 for zero-latency flash seeks.


1. Virtual Memory Architecture & Database I/O

When a database writes data, the Linux kernel buffers modified pages in RAM before syncing them to NVMe storage. Default kernel flushing behavior is disastrous for databases:

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    DB["PostgreSQL / MariaDB<br/>shared_buffers Flush"]
    PAGES["2 MiB Huge Pages in RAM<br/>(Zero TLB Thrashing)"]
    FLUSH{"Kernel Dirty Buffer Check<br/>vm.dirty_background_ratio: 5%"}
    BG_SYNC["Asynchronous Kernel Flusher<br/>Continuous Smooth Streaming"]
    NVME["PCIe 4.0/5.0 NVMe Controller<br/>(Scheduler: none / direct queue)"]

    DB --> PAGES
    PAGES --> FLUSH
    FLUSH --> BG_SYNC
    BG_SYNC --> NVME

Eliminating the "Dirty Page Flush Stall"

On a server with 128 GB of RAM, the default Linux setting vm.dirty_ratio = 20 allows up to 25.6 GB of unwritten dirty pages to accumulate in memory before blocking applications. When the threshold is reached, the kernel freezes all writing processes and floods the storage bus with a massive synchronous write burst, causing latency spikes of 5 to 30 seconds.

Production Solution: Force the kernel to wake background flushers continuously at low thresholds: - vm.dirty_background_ratio = 5: Flusher threads start writing when dirty data reaches 5% of RAM. - vm.dirty_ratio = 10: Applications are throttled only if dirty data reaches 10% of RAM.


2. Kernel sysctl.conf Production Template

Deploy these optimized virtual memory and IPC settings on any dedicated Debian 12 database server:

# File: /etc/sysctl.d/99-database-kernel-tuning.conf
# ----------------------------------------------------------------------
# 1. Memory Swapping & Overcommit Controls
# ----------------------------------------------------------------------
# Aggressively avoid swapping shared_buffers out to swap space
vm.swappiness = 1

# Prevent the kernel OOM killer from panicking under normal database spikes
vm.overcommit_memory = 2
vm.overcommit_ratio = 80

# ----------------------------------------------------------------------
# 2. Smooth Background Dirty Page Flushing (Prevents I/O Freezes)
# ----------------------------------------------------------------------
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10

# ----------------------------------------------------------------------
# 3. Inter-Process Communication (IPC) & Shared Memory Limits
# ----------------------------------------------------------------------
# Maximum size of a single shared memory segment (allow up to 90% of RAM)
kernel.shmmax = 68719476736

# System-wide total amount of shared memory pages (calculated for 64GB+)
kernel.shmall = 16777216

# ----------------------------------------------------------------------
# 4. Network Socket Queue Buffers for High-Concurrency Connections
# ----------------------------------------------------------------------
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

Apply immediately without rebooting:

sudo sysctl --system

3. 2 MiB Huge Pages: Eliminating TLB Cache Misses

By default, the Linux x86-64 kernel manages memory in 4 KiB pages. When PostgreSQL allocates a shared_buffers = 32GB pool, the CPU Translation Lookaside Buffer (TLB) must track:

(32 × 1024 × 1024 KiB) / 4 KiB = 8,388,608 page table entries

Navigating 8.3 million page entries causes massive CPU cache line evictions. By switching to 2 MiB Huge Pages, the total page count drops to 16,384 entries (a 512× reduction), fitting comfortably inside CPU L2/L3 caches.

Step 1: Calculate Required Huge Pages

For a 16 GB shared_buffers pool:

HugePages = (16 × 1024 MB) / 2 MB = 8192 pages

Allocate with an 8% safety buffer (8,800 pages):

# File: /etc/sysctl.d/99-hugepages.conf
vm.nr_hugepages = 8800

Step 2: Disable Transparent Huge Pages (THP)

Transparent Huge Pages (THP) allocate 2 MiB pages dynamically at runtime, but frequently cause random, multi-second memory compaction stalls. Always disable THP for database servers:

# Verify current THP status:
cat /sys/kernel/mm/transparent_hugepage/enabled
# If [always], disable persistently via systemd unit or GRUB parameter:
# grub: transparent_hugepage=never

Step 3: Enable in postgresql.conf

# File: /etc/postgresql/17/main/postgresql.conf
huge_pages = on   # Fails to start if HugePages are insufficient (prevents silent 4 KiB fallback)

4. NVMe Multi-Queue I/O Scheduler & Filesystem Tuning

Modern enterprise NVMe SSDs utilize the PCIe bus to communicate with up to 64,000 independent hardware queues simultaneously. Legacy Linux elevator schedulers (such as cfq or single-queue deadline) serialize I/O, creating artificial bottlenecks.

# Check current scheduler for nvme0n1:
cat /sys/block/nvme0n1/queue/scheduler
# Output: [none] mq-deadline

For NVMe devices, the scheduler should be set to none (pure hardware queue pass-through) or mq-deadline:

# File: /etc/udev/rules.d/60-nvme-scheduler.rules
# Assign 'none' scheduler to all high-speed PCIe NVMe drives
ACTION=="add|change", KERNEL=="nvme[0-9]*n[0-9]*", ATTR{queue/scheduler}="none"

Filesystem Mount Options (/etc/fstab)

When mounting filesystems hosting database data directories (/var/lib/postgresql or /var/lib/mysql), disable access-time write overhead:

# /etc/fstab
UUID=xxxx-xxxx-xxxx  /var/lib/postgresql  ext4  noatime,nodiratime,commit=60,errors=remount-ro  0  2
- noatime,nodiratime: Eliminates disk write I/O on every single SELECT read. - commit=60: Flushes ext4 filesystem metadata deltas every 60 seconds instead of the default 5 seconds (database WAL handles crash safety independently).


5. PostgreSQL Planner Calibration for NVMe Storage

By default, PostgreSQL assumes data resides on mechanical, rotating magnetic hard drives where random seeks are 4× more expensive than sequential scans. On NVMe SSDs, random seeks occur at hardware bus speed with zero mechanical seek penalty.

Update these parameters in postgresql.conf:

# File: /etc/postgresql/17/main/postgresql.conf

# 1. Random seek cost is almost identical to sequential read on NVMe (Default was 4.0!)
random_page_cost = 1.1

# 2. Allow kernel to pre-fetch up to 200 physical pages concurrently across NVMe queues
effective_io_concurrency = 200

# 3. Inform the cost-based planner how much RAM is available for caching (75% of total RAM)
effective_cache_size = 48GB

# 4. Asynchronous behavior for analytical workers
max_worker_processes = 8
max_parallel_workers_per_gather = 4
max_parallel_maintenance_workers = 4

Reload PostgreSQL to apply:

sudo systemctl reload postgresql