MariaDB and MySQL Tuning for a Small VPS
MySQL and MariaDB both ship with a configuration designed to start on almost anything, including a laptop from 2009. The default innodb_buffer_pool_size is 128 MB. That single number means a database on a 2 GB VPS is using a sixteenth of the RAM it could be, reading pages from disk that should be sitting in memory. The result is a database that feels sluggish on a workload that should fit comfortably in cache.
The good news is that you do not need a DBA to fix this. A handful of well-chosen settings get you most of the way, and the rest is observability. This walkthrough covers what to change, why, how to apply it safely, and how to find the slow queries that no amount of tuning will save you from. MariaDB is a fork of MySQL, so the knobs are almost identical - the few places they differ are flagged as we go.
TL;DR
- Raise
innodb_buffer_pool_sizeto 60-70% of RAM on a dedicated DB box, or 25-40% if the app shares the machine. - Set
innodb_flush_log_at_trx_commit = 2for a big write speedup if you can tolerate losing ~1 second of data on a power loss. - Use
innodb_flush_method = O_DIRECTand, on NVMe, raiseinnodb_io_capacity. - Keep
max_connectionsmodest and the per-connection buffers small - they multiply. - Leave the query cache off. Turn on the slow query log and run
mysqltunerafter a day of real traffic.
Total time: around 30 minutes, plus one restart.
What You Need
- A VPS running MySQL 8.0 or MariaDB 10.5+ (the values apply to the 10.6 and 10.11 LTS releases and to 11.x too).
- Root or sudo access on the host.
- An idea of how much RAM you have.
free -hwill tell you. - To know whether the box is database-only or shares RAM with your application.
- A recent backup or a snapshot before you change anything. Our encrypted Restic backups guide covers off-site dumps you can roll back to.
We assume Ubuntu 22.04 or 24.04. Only the file paths are distro-specific; the settings themselves are the same everywhere.
Step 1: Find the Config File
MySQL and MariaDB read a chain of files. Do not edit the packaged ones - drop your own file in so a package upgrade never clobbers your work. The include directory depends on the distro and the engine:
- Debian and Ubuntu MariaDB:
/etc/mysql/mariadb.conf.d/ - Debian and Ubuntu MySQL:
/etc/mysql/mysql.conf.d/ - RHEL, Rocky, Alma:
/etc/my.cnf.d/
If you are not sure what your server reads, ask it for the search order:
mysqld --verbose --help 2>/dev/null | grep -A1 "Default options"
Create a dedicated drop-in. The 99- prefix makes it sort last, so it wins any conflict:
# MariaDB
sudo nano /etc/mysql/mariadb.conf.d/99-tuning.cnf
# MySQL
sudo nano /etc/mysql/mysql.conf.d/99-tuning.cnf
Everything below goes under a [mysqld] header in that file.
Step 2: Know Your Version and Engine
SELECT VERSION();
Then confirm your tables are actually InnoDB. Everything in this guide tunes InnoDB; it does nothing for the old MyISAM engine, which uses key_buffer_size instead.
SELECT table_schema, engine, COUNT(*)
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY table_schema, engine;
If you see MyISAM on real application tables, plan a migration to InnoDB (covered in Going Further). InnoDB has been the default since MySQL 5.5, so most databases are already there.
Step 3: The InnoDB Buffer Pool
This is 80% of the win. The buffer pool caches table and index pages in RAM, and the default 128 MB is the first thing to fix.
[mysqld]
innodb_buffer_pool_size = 1G
How to size it:
- Dedicated DB box: 60-70% of RAM. The operating system and the rest of MySQL need the remainder.
- Shared box (app + DB together): 25-40%. Leave room for PHP or Node, your application, and the kernel page cache.
On a 2 GB VPS running both an app and the database, 512M is a sane start. On a 4 GB database-only box, 2560M (around 62%) is reasonable. One caveat: do not exceed your data size. If the whole database is 400 MB, a 4 GB buffer pool just wastes RAM - size it to the data plus headroom for growth.
You can skip innodb_buffer_pool_instances. Splitting the pool only reduces contention above ~1 GB, and MariaDB 10.5+ ignores the setting entirely. On a small VPS, leave it at the default.
The buffer pool is resizable online (since MySQL 5.7 and MariaDB 10.2), so you can test a value before committing to a restart:
SET GLOBAL innodb_buffer_pool_size = 1073741824; -- 1 GiB, in bytes
SHOW STATUS LIKE 'Innodb_buffer_pool_resize_status';
Once the resize finishes, write the value into your .cnf so it survives a restart.
Step 4: Redo Log and Flushing
The redo log absorbs writes before they are checkpointed into the tablespace. The default is small, which forces frequent checkpoints and caps write throughput. MariaDB and recent MySQL name this setting differently:
# MariaDB 10.5+ (single redo log)
innodb_log_file_size = 512M
# MySQL 8.0.30+
innodb_redo_log_capacity = 536870912 # 512 MiB in bytes
A larger redo log means fewer checkpoint flushes and smoother writes; the cost is a slightly longer crash recovery. 256-512 MB is a good range for a small box.
Then the durability dial, the biggest single write speedup here:
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commitcontrols how aggressively each commit is persisted.1(the default) is full ACID: flush andfsyncon every commit.2flushes to the OS on every commit but onlyfsyncs about once a second - far less disk pressure, at the cost of losing up to a second of committed transactions if the whole machine loses power.0is similar but loses that second even on a plainmysqldcrash. For anything that is not a payment ledger,2is the right trade.innodb_flush_method = O_DIRECTstops InnoDB pages being cached twice (once in the buffer pool, once in the OS page cache). On Linux with a normal filesystem this is almost always correct.
Step 5: Tell InnoDB It Is on NVMe
The I/O defaults assume a single slow spinning disk. Any modern VPS is on NVMe, so raise the ceiling:
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_flush_neighbors = 0
innodb_io_capacityis the steady-state budget for background flushing. The default200suits one rotational disk; NVMe handles thousands.innodb_flush_neighbors = 0disables the optimisation that flushes adjacent pages together. That batching only helps on rotational disks where seeking is expensive. On flash it just writes pages you did not need to.
Step 6: Connections and Per-Connection Buffers
The default max_connections = 151 is rarely right for a small box. Each connection costs memory before it runs a single query, and several buffers are allocated per connection.
max_connections = 80
thread_cache_size = 16
Then the dangerous ones - keep them small:
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 1M
These are allocated per connection, and sort_buffer_size can be allocated more than once inside a single complex query. Raising them globally to "go faster" is exactly how a box with plenty of idle RAM suddenly OOMs under load. The worst-case memory is roughly:
innodb_buffer_pool_size
+ max_connections * (sort_buffer_size + join_buffer_size
+ read_buffer_size + read_rnd_buffer_size + thread_stack)
If one report genuinely needs a big sort, raise the buffer for that session only, never globally:
SET SESSION sort_buffer_size = 16 * 1024 * 1024;
Step 7: Leave the Query Cache Off
This one is a "do not". The old MySQL query cache was removed entirely in MySQL 8.0. MariaDB still has it, but it is off by default and should stay that way on any write-heavy or concurrent workload - its single global mutex becomes a bottleneck that is usually slower than no cache at all.
# MariaDB only; this is already the default - do not turn it on
query_cache_type = 0
query_cache_size = 0
If you want a query cache, put it in your application (Redis, or your framework's cache layer), not in the database.
Step 8: Temp Tables and the Table Cache
Queries that group, sort, or use UNION may build temporary tables. If they outgrow the in-memory limit they spill to disk, which is slow.
tmp_table_size = 64M
max_heap_table_size = 64M
table_open_cache = 4000
open_files_limit = 8192
tmp_table_size and max_heap_table_size must be raised together - the effective limit is the lower of the two. Watch Created_tmp_disk_tables against Created_tmp_tables to see whether you are spilling to disk.
Step 9: Apply the Changes
Save the drop-in, then restart:
sudo systemctl restart mariadb # MariaDB
sudo systemctl restart mysql # MySQL
Many of these settings are dynamic, so you can apply them at runtime to test before persisting. Set the live value, confirm it, then write it into your .cnf:
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
SET GLOBAL max_connections = 80;
On MySQL 8 you can make a runtime change durable in one step - it writes to mysqld-auto.cnf:
SET PERSIST innodb_io_capacity = 2000;
MariaDB has no SET PERSIST; use SET GLOBAL for the live value and edit your .cnf for the permanent one. The buffer pool, IO capacity, flush-at-commit, and connection limits are all dynamic. The redo log size and innodb_flush_method are applied on restart.
Step 10: Turn On the Slow Query Log
Tuning settings is half the job. The other half is finding the queries that are slow.
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 0
long_query_time = 1 logs anything over a second. Turn log_queries_not_using_indexes on only briefly - on a busy server it floods the log. Make sure the file is writable by the server:
sudo touch /var/log/mysql/slow.log
sudo chown mysql:mysql /var/log/mysql/slow.log
You can enable it without a restart:
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
After a day of real traffic, find the worst offenders. mysqldumpslow ships with the server:
sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
For a far better report, Percona's pt-query-digest groups queries by fingerprint and shows where the time actually goes:
sudo apt install -y percona-toolkit
sudo pt-query-digest /var/log/mysql/slow.log | less
Step 11: Read a Plan With EXPLAIN
Once you have a suspect query, see how the engine runs it:
EXPLAIN
SELECT u.id, u.email, COUNT(o.id) AS orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL 30 DAY
GROUP BY u.id, u.email
ORDER BY orders DESC
LIMIT 50;
Sample output (trimmed):
+----+-------+------+---------------+------+-------+----------------------------------+
| id | table | type | possible_keys | key | rows | Extra |
+----+-------+------+---------------+------+-------+----------------------------------+
| 1 | u | ALL | NULL | NULL | 48211 | Using where; Using temporary; |
| | | | | | | Using filesort |
| 1 | o | ref | user_id_idx | ... | 4 | |
+----+-------+------+---------------+------+-------+----------------------------------+
Things to look for:
type: ALLis a full table scan. On a large table that wants a filter, add the missing index - here, onusers.created_at.Using filesortandUsing temporarymean the sort or grouping could not use an index. Often a single composite index removes both.- A
rowsestimate wildly off the real count means stale statistics. RunANALYZE TABLE users;.
For the actual execution rather than an estimate, MySQL 8.0.18+ has EXPLAIN ANALYZE, and MariaDB has the ANALYZE statement plus ANALYZE FORMAT=JSON.
Step 12: Verify With mysqltuner
After the server has been up under real load for a day, run MySQLTuner. It reads your live status counters and flags what is still wrong:
wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl
sudo perl mysqltuner.pl
Pay attention to its "Maximum possible memory usage" line - that is the OOM ceiling from Step 6. If it exceeds your RAM, lower max_connections or the per-connection buffers.
The single most useful health number is the buffer pool hit ratio:
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
Divide Innodb_buffer_pool_reads (pages fetched from disk) by Innodb_buffer_pool_read_requests (total page reads). A healthy small box reads from disk well under 1% of the time. If that ratio stays high after the cache has warmed, the buffer pool is too small for your working set.
Troubleshooting
MySQL will not start after the change. Almost always the buffer pool plus buffers exceed RAM, or there is a typo in the .cnf. Read the error log with sudo journalctl -u mariadb -n 50 (or -u mysql). Comment out your drop-in, start the server, and re-add settings one block at a time.
The OOM killer is killing mysqld under load. Total memory is over budget. Lower innodb_buffer_pool_size, drop max_connections, and shrink the per-connection buffers, then re-run mysqltuner's memory line.
Changes seem ignored. Another file in the include chain overrides yours. Confirm the effective value with SHOW VARIABLES LIKE '...';, make sure your drop-in sorts last, and check it sits under [mysqld].
The first queries after a restart are slow, then fast. Cold buffer pool. It refills as pages are read. To warm it automatically, keep innodb_buffer_pool_dump_at_shutdown and innodb_buffer_pool_load_at_startup on (both default to ON in modern versions).
Too many connections errors. The app is opening more connections than max_connections allows. Either it is leaking connections (fix the pool), or raise the limit - but mind the memory math. A connection pooler is the real fix.
Going Further
- Put a connection pooler (ProxySQL, or your framework's persistent pool) in front of the database so a small
max_connectionscan serve a busy app without exhausting RAM. This often beats any single.cnftweak. - Migrate leftover MyISAM tables to InnoDB with
ALTER TABLE mytable ENGINE=InnoDB;. You get row-level locking, crash recovery, and the buffer pool you just sized. - Dashboard the metrics so you spot regressions before users do. Our guide to Grafana and Prometheus on a VPS plus
mysqld_exportergives you buffer pool, connection, and slow-query graphs. - On Postgres instead? Same exercise, different knobs - see PostgreSQL Tuning for a Small VPS.
- Once the box is tuned, make sure it is also locked down. Our SSH hardening with Fail2Ban walkthrough closes the front door.
A well-tuned MariaDB on a $5 NVMe VPS will comfortably outrun a default install on far bigger hardware. The buffer pool alone gets you most of the way; the slow query log gets you the rest.
Need a fast box to run MariaDB or MySQL on? Our Linux VPS plans ship with NVMe storage, generous RAM, and snapshots so you can resize the buffer pool without fear.