Default PostgreSQL on a 32 GB VPS uses 128 MB of shared memory. Default Redis happily swaps to disk when memory gets tight. Out of the box, neither uses the RAM you paid for. Getting the most from a high-RAM box is 20% installation and 80% tuning — the kind of tuning that turns a query from 8 seconds into 30 ms, or a Redis cache from 70% hit rate into 99%.
This guide walks the full path: kernel-level memory tuning (swappiness, hugepages, dirty page ratios), PostgreSQL configuration for a memory-rich environment, Redis persistence and eviction policies, and the benchmarking workflow that proves your tuning actually helped. Specific numbers given throughout — not generic 'tune to your workload' advice. By the end, your high-RAM VPS will be using its memory the way it was designed to.
- A Linux VPS with at least 16 GB RAM — Ubuntu 22.04 or Debian 12
- Comfort with
sysctl,postgresql.conf, andredis.conf - Roughly 45 minutes (plus benchmarking time)
For pre-tuned database workloads, our High RAM VPS plans ship with these kernel parameters already in place — 16, 32, or 64 GB DDR4 ECC.
1. Why default settings waste your RAM
Both PostgreSQL and Redis ship with conservative defaults that assume "you might be running this on a 512 MB toy VPS." On a 32 GB or 64 GB box, those defaults leave 80% of your RAM as idle file-cache pages — useful, but not what you bought the box for.
Concrete impact: PostgreSQL shared_buffers default is 128 MB. Run a query against a 10 GB hot table; Postgres reads from disk every time because the table doesn't fit in its buffer pool. Bump shared_buffers to 16 GB and the same query runs 50–100× faster on warm cache. That's not optimization theatre — that's the actual reason high-RAM VPS plans exist.
2. Kernel-level memory tuning
These go in /etc/sysctl.d/99-database-tuning.conf and survive reboots:
cat > /etc/sysctl.d/99-database-tuning.conf <<'EOF'
# Discourage swapping (databases hate it)
vm.swappiness = 1
# Database writes: smaller dirty page ratios prevent IO storms
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
vm.dirty_expire_centisecs = 1000
# Prevent OOM killer from picking the DB
vm.overcommit_memory = 2
vm.overcommit_ratio = 90
# Larger shared memory region for Postgres
kernel.shmmax = 17179869184 # 16 GB
kernel.shmall = 4194304 # 16 GB in 4 KB pages
EOF
sysctl --system
Each setting matters:
- swappiness=1 — Linux still has emergency swap available, but never proactively swaps. Databases swapping out hot buffer pages is catastrophic for latency.
- dirty_background_ratio=5 — start flushing dirty pages to disk when they hit 5% of RAM (vs default 10%). Smoother write IO, no spikes.
- overcommit_memory=2 — Linux refuses memory allocations beyond physical RAM × overcommit_ratio. Prevents the OOM killer from picking your DB at 3 AM when some other process leaks.
- shmmax / shmall — older Postgres versions used SysV shared memory; modern Postgres mostly uses POSIX, but setting these keeps both happy.
3. Transparent hugepages (and why to disable them)
Counterintuitive: turn THP off for databases. Transparent Hugepages were designed to help applications with large memory footprints by transparently combining pages. Sounds great. In practice, the kernel's khugepaged scanner causes latency spikes during compaction — measured 10–500 ms stalls on busy database workloads.
# Disable until reboot
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# Make permanent via a systemd service
cat > /etc/systemd/system/disable-thp.service <<'EOF'
[Unit]
Description=Disable Transparent Hugepages
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=mongod.service postgresql.service redis-server.service
[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=basic.target
EOF
systemctl enable disable-thp
Static hugepages (the explicit kind) are different and worth using for Postgres at large scale. We cover that in the Postgres section.
4. Install PostgreSQL
Use the official Postgres repo (their builds are newer and better-supported than distro packages):
install -d /usr/share/postgresql-common/pgdg
curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
--fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list'
apt update
apt install -y postgresql-16
For initial hardening (auth, listen addresses, pg_hba.conf), follow our PostgreSQL setup guide — same steps apply on any RAM-tier VPS.
5. Tune PostgreSQL for high RAM
Edit /etc/postgresql/16/main/postgresql.conf. The values below assume a 32 GB RAM VPS dedicated to Postgres. Scale proportionally for 16 GB or 64 GB.
# === Memory ===
# 25-30% of total RAM — the workhorse setting
shared_buffers = 8GB
# How much memory the planner thinks the OS will cache
# Roughly 50-60% of RAM
effective_cache_size = 20GB
# Per-operation memory for sorts and joins. Multiply by concurrent connections!
# 64 MB * 100 concurrent queries = 6.4 GB max. Tune to your concurrency.
work_mem = 64MB
# For maintenance ops (VACUUM, CREATE INDEX, ALTER TABLE)
maintenance_work_mem = 2GB
# WAL settings — bigger WAL = fewer checkpoints = better write throughput
wal_buffers = 64MB
min_wal_size = 2GB
max_wal_size = 8GB
# Checkpoint timing — spread writes over more time
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min
# === Parallel query (use multiple cores per query) ===
max_worker_processes = 8
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
# === Connections (memory cost per connection) ===
max_connections = 200
# === Logging slow queries — invaluable for tuning ===
log_min_duration_statement = 500 # log queries over 500ms
log_checkpoints = on
log_lock_waits = on
log_temp_files = 0
# === Stats for query planner ===
default_statistics_target = 200
Restart Postgres and verify:
systemctl restart postgresql
# Check the effective settings
sudo -u postgres psql -c "SELECT name, setting, unit FROM pg_settings
WHERE name IN ('shared_buffers','effective_cache_size','work_mem',
'maintenance_work_mem','wal_buffers','max_wal_size');"
If you want explicit hugepages for Postgres (recommended for production at 32 GB+):
# Calculate needed pages: shared_buffers / page size (usually 2 MB)
# 8 GB shared_buffers / 2 MB = 4096 pages, add 10% buffer = 4500
echo "vm.nr_hugepages = 4500" >> /etc/sysctl.conf
sysctl -p
# Verify
grep Huge /proc/meminfo
Add huge_pages = on to postgresql.conf and restart. Postgres now uses 2 MB hugepages explicitly — bigger TLB efficiency, no khugepaged latency spikes.
🐾 Skip the kernel-tuning rabbit hole
Our High RAM VPS plans ship with all these kernel parameters applied by default — vm.swappiness, hugepages, dirty page ratios, the lot. Same hardware (DDR4 ECC, dedicated cores), 45 minutes of tuning skipped.
See High RAM Plans →6. Install Redis
apt install -y redis-server
Redis is single-threaded for command processing. You can't make a single Redis instance use more than one core. For multi-core workloads, run multiple Redis instances on different ports, or use Redis Cluster.
7. Tune Redis: persistence + eviction
Edit /etc/redis/redis.conf:
# === Memory ===
# Set to roughly 50% of available RAM after Postgres reserves its share
maxmemory 12gb
# What to do when memory fills up
# allkeys-lru: evict any key by LRU (good for cache use)
# volatile-lru: only evict keys with TTL (good for mixed cache + persistent data)
# noeviction: refuse writes (good for persistent-only)
maxmemory-policy allkeys-lru
# === Persistence ===
# AOF (append-only file) — survives crashes with near-zero data loss
appendonly yes
appendfsync everysec # fsync once per second; balance of safety and speed
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# RDB (point-in-time snapshots) — keep for backups even with AOF
save 3600 1
save 300 100
save 60 10000
# === Performance ===
# Disable transparent hugepages reminder
# (already disabled in step 3)
# IO threads (Redis 6+) — parallelize network IO
io-threads 4
io-threads-do-reads yes
# === Network ===
# Listen only on localhost by default; use systemd or firewall for public access
bind 127.0.0.1 ::1
protected-mode yes
port 6379
# === Slow log — invaluable for debugging ===
slowlog-log-slower-than 10000 # log commands over 10ms
slowlog-max-len 128
Restart Redis and verify:
systemctl restart redis-server
# Confirm it's running with the new config
redis-cli INFO memory | head -20
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET appendonly
8. Benchmark before and after
"Did my tuning work" — only answerable with numbers.
PostgreSQL: pgbench
sudo -u postgres pgbench -i -s 100 postgres # initialize a 1.5 GB test DB
# Read-heavy benchmark, 50 concurrent clients, 60 seconds
sudo -u postgres pgbench -c 50 -j 4 -T 60 -S postgres
# Read/write mix
sudo -u postgres pgbench -c 50 -j 4 -T 60 postgres
The output gives you transactions per second (tps). Run before tuning, run after. Tuning a 32 GB box from defaults to the values above typically gets 5–10× tps improvement on read-heavy workloads.
Redis: redis-benchmark
# 1 million SET/GET operations across 50 concurrent clients
redis-benchmark -t set,get -n 1000000 -c 50 -q
# Pipelined ops (16 commands batched per round-trip)
redis-benchmark -t set,get -n 1000000 -c 50 -P 16 -q
Modern Redis on tuned hardware hits 100,000+ ops/sec per core for non-pipelined, 500,000+ with pipelining.
9. Reference: tuning cheat sheet
RAM allocation rule of thumb
| VPS RAM | Postgres shared_buffers | Redis maxmemory | OS reserved |
|---|---|---|---|
| 16 GB | 4 GB | 6 GB | ~6 GB |
| 32 GB | 8 GB | 12 GB | ~12 GB |
| 64 GB | 16 GB | 24 GB | ~24 GB |
Why so much OS reserved? Because effective_cache_size includes the OS page cache — leaving plenty of RAM unallocated means it gets used as file cache, accelerating disk reads for tables that don't fit in shared_buffers.
Connection pool sizing
Each Postgres connection allocates ~10 MB of overhead plus work_mem when executing. With work_mem = 64 MB and 200 max_connections, you could in theory burn 12 GB on work_mem alone during peak concurrency. Use PgBouncer as a connection pooler in front of Postgres — apps connect to PgBouncer (light), PgBouncer holds maybe 30–50 actual Postgres connections.
Monitoring
Once tuned, monitor continuously. pg_stat_statements for top queries, pg_stat_io for buffer hit rates, INFO memory for Redis. A buffer hit rate below 99% means you need more shared_buffers (or a bigger VPS).
When to scale beyond a single VPS
One VPS handles a surprising amount — at the 64 GB tier, Postgres datasets up to ~30 GB hot working set, Redis caches up to ~24 GB. Beyond that, options:
- Move Redis to its own VPS
- Set up streaming replication for Postgres (read replica on another VPS)
- Upgrade to a dedicated server — 256 GB+ RAM, same KVM stack
ECC vs non-ECC RAM
ECC corrects single-bit memory errors silently and reports multi-bit errors. On a database workload running 24/7, the probability of a bit-flip during a year is non-trivial. ECC catches them; non-ECC can corrupt your data invisibly. Our High RAM plans use ECC — most consumer VPS providers don't.
FAQ
Why disable transparent hugepages but enable explicit hugepages?
Transparent hugepages (THP) cause unpredictable latency spikes because the kernel's khugepaged scanner runs in the background, defragmenting and combining pages. For databases that care about p99 latency, those 50–500 ms stalls are unacceptable. Explicit hugepages (the static kind you allocate via vm.nr_hugepages) give you the TLB efficiency benefits without the scanner — pages are pre-allocated and never moved.
How do I know if shared_buffers is too small?
Check the buffer hit rate: SELECT sum(blks_hit) / sum(blks_hit + blks_read) FROM pg_stat_database;. Above 99% = healthy. Below 95% = increase shared_buffers (or upgrade RAM if you're already maxed). Below 90% = your hot dataset doesn't fit at all and you're hitting disk constantly.
Should I use ZFS for the Postgres data directory?
Possible but tricky. ZFS gives you snapshots, compression, and checksums — all genuinely useful for a database. But ZFS uses its own ARC cache which competes with Postgres's shared_buffers. If you go ZFS, cap the ARC at ~25% of RAM with zfs_arc_max, and don't set Postgres full_page_writes=off (a common ZFS recommendation that's risky). For most setups, ext4 or XFS is simpler.
Can I run Postgres + Redis + my app on one VPS?
Yes, but be honest about RAM math. A 32 GB box with Postgres at 8 GB shared_buffers + Redis at 12 GB + 8 GB for the OS leaves only 4 GB for your app and connection overhead. For anything beyond a small app, separate Redis to its own (small) VPS or use a dedicated DB tier.
Does Redis really need persistence?
If Redis holds cache-only data that can be regenerated, no — persistence just wastes IO. If it holds session data, queues, or anything you'd be sad to lose, yes. The appendfsync everysec AOF mode is the standard compromise: at most 1 second of data loss in a crash, minimal performance overhead.
What about kernel scheduling and CPU affinity?
For a single Postgres instance, the default kernel scheduler is fine. Multiple instances or multi-tenant DB hosts benefit from CPU affinity (taskset) to keep each Postgres backend on dedicated cores — reduces context-switch overhead. This is advanced territory; only worth doing if you've already maxed out the easier tuning.