MariaDB Performance Tuning for Production: Memory, InnoDB, I/O, and Docker

Published

Measure → Calculate → Configure → Load test → Observe → Tune again. Every important variable: what it does, the default, what goes wrong, and how to calculate the right value.

MariaDB performance tuning is often reduced to numbers copied from someone else's my.cnf:

innodb_buffer_pool_size = 16G
max_connections = 500
tmp_table_size = 1G
sort_buffer_size = 8M

It looks reasonable. It may even work. And it can still be completely wrong.

A MariaDB instance on a dedicated 128 GB database server has very different resource boundaries from one inside a 16 GB Docker container on a host that also runs Redis, Nginx, application workers, and backup jobs. Good tuning is about the relationship between infrastructure, workload, MariaDB internals, OS limits, and application architecture — not memorizing recommended values.

Tuning loop:
Measure → Calculate → Configure → Load test → Observe → Tune again
The final my.cnf is only the last step.

For every important setting in this guide you get four things:

  1. What it does — in plain language
  2. Default value — upstream MariaDB default if you omit it
  3. What goes wrong — if you leave the default or set it wrong
  4. How to calculate — step-by-step with real numbers

Part 1 — Versions, setup, and configuration layout

For production, use an actively maintained LTS branch rather than an old non-LTS release just because an older tuning guide happens to use it.

MariaDB 10.11, 11.4, and 11.8 are the relevant Community Server LTS lines discussed here. MariaDB 11.4 was explicitly introduced as an LTS release; MariaDB 11.8 is the newer Community LTS line. MariaDB's current release information shows active 2026 maintenance releases for 10.11, 11.4, and 11.8.

MariaDB Community Server 10.6 reached end of life on July 6, 2026. If you are starting a new Community deployment now, 10.6 should not be your baseline. Older versions are mentioned only when a variable changed, was deprecated, or was removed.

Always check the exact version first

SELECT VERSION();

mariadbd --version

Do not assume that MariaDB 11.x means every 11.x release behaves exactly the same. Some variables changed during maintenance releases too.

MariaDB exposes system-variable metadata through information_schema.SYSTEM_VARIABLES, which is extremely useful when you need the actual default, current value, scope, and origin:

SELECT
    VARIABLE_NAME,
    GLOBAL_VALUE,
    DEFAULT_VALUE,
    VARIABLE_SCOPE,
    GLOBAL_VALUE_ORIGIN
FROM information_schema.SYSTEM_VARIABLES
WHERE VARIABLE_NAME IN (
    'innodb_buffer_pool_size',
    'innodb_log_file_size',
    'innodb_log_buffer_size',
    'innodb_io_capacity',
    'max_connections',
    'tmp_table_size',
    'max_heap_table_size',
    'sort_buffer_size',
    'join_buffer_size'
);
Important distinction: MariaDB compiled/server default is not necessarily the same as the default configuration supplied by Debian/RHEL/Docker image. An OS package or image can ship a .cnf file that overrides the upstream server default.

Find the configuration MariaDB actually reads

Do not assume the main file is always /etc/my.cnf. Depending on installation and distribution, common locations include:

/etc/my.cnf
/etc/mysql/my.cnf
/etc/mysql/mariadb.conf.d/
/etc/mysql/conf.d/
/etc/my.cnf.d/

Check the actual search path:

mariadbd --help --verbose 2>/dev/null | grep -A20 "Default options are read from"

Then inspect effective startup options:

mariadbd --print-defaults
# or
my_print_defaults mysqld

The exact file hierarchy matters because later option files can override values loaded earlier.

Recommended configuration layout

For a package-based Linux installation, avoid editing vendor files whenever possible. Create your own file:

Use [mariadbd] for modern deployments when the server reads that group. If the existing installation uses another server option group, follow the existing configuration layout.

For Docker, keep configuration outside the container image:

mariadb/
├── conf.d/
│   └── 99-production-tuning.cnf
└── compose.yaml

services:
  mariadb:
    image: mariadb:11.8
    volumes:
      - ./conf.d:/etc/mysql/conf.d:ro
      - mariadb-data:/var/lib/mysql

Do not SSH into a running container and edit /etc/mysql/... manually — that change disappears when the container is replaced. A production configuration should be version controlled, reproducible, and mounted or baked intentionally.

Part 2 — Resource boundaries and memory budget

Before calculating any MariaDB parameter, determine the resource boundary. There are three different questions:

  1. How much RAM does the physical host have?
  2. How much RAM is available to the VM?
  3. How much RAM is actually available to the MariaDB process/container?

They are not necessarily the same.

Measure RAM on Linux

free -h
cat /proc/meminfo

For Docker:

docker inspect <container> --format '{{.HostConfig.Memory}}'

If the result is non-zero, it represents the container's memory limit in bytes.

For Kubernetes:

kubectl get pod <pod> -o yaml
# look for resources.limits.memory

The database must be sized against the resource boundary that the MariaDB process actually has.

Build a memory budget

Example host: 64 GiB RAM · also runs Nginx, PHP-FPM, Redis, application workers, Docker, backup agent, log collector
MariaDB Budget = Available RAM − Other Services − OS Reserve − Operational Headroom
Host RAM = 64 GiB
Other services = 12 GiB
OS/filesystem reserve = 4 GiB
Operational headroom = 4 GiB
─────────────────────────────────
MariaDB budget = 44 GiB

This is fundamentally better than 64 × 0.80 = 51.2 GiB because the second calculation ignores the rest of the machine.

Global vs per-connection memory

MariaDB memory is not one big pool. Think about it in two categories:

A 1 GB global cache is one thing. A 1 GB limit that can potentially be reached by hundreds of sessions is something else entirely. MariaDB's own documentation warns about the cumulative effect of per-thread buffers at high concurrency.

B + (C × P) + T + G + R ≤ M

Where B = buffer pool, C = expected peak concurrent sessions, P = estimated per-session working memory, T = temporary-table allowance, G = other global MariaDB memory, R = safety reserve, M = available MariaDB memory. This is a capacity-planning model, not an exact MariaDB allocator equation.

Part 3 — InnoDB buffer pool

The InnoDB buffer pool is normally the largest memory allocation in an InnoDB-heavy workload. It caches table data, indexes, dirty pages, and frequently accessed pages. The objective is to keep the active working set in memory and reduce physical reads.

innodb_buffer_pool_size

What it does: Allocates the InnoDB buffer pool — the primary in-memory cache for InnoDB data and indexes.

Default if omitted: 128 MiB (upstream MariaDB). Conservative server default, not a production recommendation for a large database.

What goes wrong: Too small → excessive physical reads and high latency. Too large → MariaDB competes with per-connection memory, OS page cache, and other services; swap activity under load.

How to calculate — step by step:

  1. Start with the MariaDB memory budget from Part 2.
  2. Reserve memory for connections, temporary tables, per-query operations, Performance Schema, and OS/container safety.
  3. Assign the remainder to the buffer pool.
  4. Check actual database size and buffer-pool effectiveness (below).
  5. Validate under production-like load — does increasing the pool materially reduce physical reads?
Buffer Pool = MariaDB Memory Budget − Connection/Query Budget − Temporary Memory − Global/Internal Headroom
MariaDB budget = 44 GiB · connection/query = 4 GiB · temporary = 2 GiB · headroom = 4 GiB
innodb_buffer_pool_size = 34G

Check actual database size

SELECT
    ROUND(
        SUM(data_length + index_length) / 1024 / 1024 / 1024,
        2
    ) AS innodb_data_and_index_gb
FROM information_schema.tables
WHERE engine = 'InnoDB';

If the result is 28.4 GiB and the buffer pool is 34 GiB, the entire current logical data/index footprint can potentially fit. That is useful information, but not enough — the database may grow, there may be fragmentation, and the working set may differ from total size.

Measure buffer pool effectiveness

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

Useful counters: Innodb_buffer_pool_read_requests and Innodb_buffer_pool_reads.

Physical Read Ratio % = (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) × 100
Innodb_buffer_pool_reads = 100,000 · Innodb_buffer_pool_read_requests = 100,000,000
→ 0.1% of logical read requests required physical reads

Do not treat one ratio as a universal target. A workload doing large sequential scans behaves very differently from a small OLTP workload. The right question: does increasing the buffer pool materially reduce physical reads and improve application latency?

Deprecated: innodb_buffer_pool_instances and innodb_buffer_pool_chunk_size

innodb_buffer_pool_instances was deprecated in MariaDB 10.5.1 and removed in 10.6. Modern MariaDB runs the buffer pool as a single instance — do not configure it on MariaDB 10.6+.

innodb_buffer_pool_chunk_size became autosized from MariaDB 10.8. On current LTS releases (10.11.12+, 11.4.6+, 11.8.2+), the variable is deprecated/ignored. Do not configure it unless you have a very specific legacy requirement.

Buffer pool warmup

After restart, the buffer pool is cold and queries hit storage until frequently used pages return to RAM. MariaDB supports:

innodb_buffer_pool_dump_at_shutdown = ON
innodb_buffer_pool_load_at_startup = ON

Both are enabled by default in current documentation. For a large production database, this is operationally useful because a restart does not necessarily mean starting with a completely cold cache.

Part 4 — Redo log sizing

The redo log should not be sized from RAM alone. First measure how much redo the workload actually generates.

SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';

Take the value, wait for a representative period (e.g. 10 minutes), then execute again.

Start = 500,000,000,000 · End = 560,000,000,000 · Duration = 600 seconds
Redo generated = 60,000,000,000 bytes → ~95.4 MiB/s average

Use peak redo rate, not average. A production database may generate 10 MiB/s normally and 100 MiB/s during peak traffic. Size based on the busiest realistic window.

Peak Redo Rate = Redo Generated During Peak Window / Peak Window Duration
Required Redo Capacity ≈ Peak Redo Rate × Desired Smoothing Window
Peak redo rate = 100 MiB/s · Smoothing window = 10 minutes
→ 100 × 600 = 60,000 MiB ≈ 58.6 GiB
At this point the question is whether a 58+ GiB redo window is operationally appropriate — not "4 GB or 8 GB?"

MariaDB has historically recommended two useful starting points: combined redo capacity ≈ 25–50% of buffer pool, or redo capacity ≈ one hour of peak redo generation. These are starting heuristics, not hard requirements. Larger redo reduces checkpoint pressure but can increase crash recovery work.

innodb_log_file_size

What it does: Sets the size of the InnoDB redo log file. On MariaDB 10.5+, this is a single file — redo capacity equals this value directly.

Default if omitted: 96 MiB on MariaDB 10.5+ (was 48 MiB in 10.4-era MariaDB).

What goes wrong: Too small → frequent checkpoints, write stalls, elevated I/O. Too large → longer crash recovery, more disk space consumed.

How to calculate:

  1. Measure peak redo rate via Innodb_os_log_written during busiest window.
  2. Multiply by desired smoothing window (e.g. 10–30 minutes).
  3. Cross-check against 25–50% of buffer pool as a secondary heuristic.
  4. Validate under staging load; from MariaDB 10.9, innodb_log_file_size is dynamically resizable.
Target redo capacity = 8 GiB → innodb_log_file_size = 8G (no multi-file calculation needed)

innodb_log_buffer_size

What it does: In-memory buffer for redo log records before they are written to disk.

Default if omitted: 16 MiB

What goes wrong: Too small → extra disk writes for large transactions. Too large → wasted memory with no benefit unless transactions justify it.

How to calculate: log_buffer_size ≥ redo generated by a large normal transaction. If the largest normal transaction generates ~30 MiB of redo, innodb_log_buffer_size = 64M is easy to justify. There is little reason to set 1G unless actual transactions justify it.

Deprecated: innodb_log_files_in_group. Older MariaDB used innodb_log_file_size × innodb_log_files_in_group (e.g. 4G × 2 = 8G). MariaDB 10.5 moved to a single redo log file; innodb_log_files_in_group was deprecated and removed in 10.6. Do not use it on modern LTS releases.

Part 5 — Durability and flush settings

innodb_flush_log_at_trx_commit

What it does: Controls when InnoDB flushes the redo log buffer to disk relative to transaction commit.

Default if omitted: 1 — write and flush redo at commit (full ACID durability).

What goes wrong: 0 or 2 can lose recent transactions during failures because redo may not have been physically flushed. Mode 2 (write at commit, flush ~once per second) can reduce flush pressure but is not universally "5–10× faster" — actual gain depends on storage latency, transaction rate, filesystem, and workload.

How to calculate: For production transactional systems where losing recently committed transactions is unacceptable, use 1. Only consider 2 when durability requirements explicitly allow it and you have validated the trade-off under your storage and workload.

sync_binlog

What it does: Controls how often the binary log is flushed to disk (when binary logging is enabled).

Default if omitted: 0 — OS controls flush timing.

What goes wrong: 0 risks losing recent binlog events on crash. 1 is safest but adds latency on write-heavy primaries.

How to calculate: For a traditional transactional primary with replication or PITR requirements, a common durability-oriented pairing is innodb_flush_log_at_trx_commit = 1 and sync_binlog = 1. Validate against storage latency. Exception: when the InnoDB-based binary log is enabled, sync_binlog is ignored.

innodb_flush_method: Historically Linux guides recommended O_DIRECT. MariaDB changed the Unix default to O_DIRECT starting with 10.6. MariaDB 11.0 deprecated innodb_flush_method in favor of granular variables (innodb_data_file_write_through, innodb_data_file_buffering, innodb_log_file_write_through, innodb_log_file_buffering). Do not blindly add O_DIRECT to a MariaDB 11.x configuration.

innodb_file_per_table defaults to ON — keep it. The explicit variable was deprecated in MariaDB 11.0.1 because the desired behavior is already the default.

innodb_doublewrite defaults to ON — protects against partial page writes. Deprecated as a tuning switch in MariaDB 11.0.1. Do not disable it because "NVMe is fast."

innodb_strict_mode defaults to ON — keep it. Not primarily a performance variable; it prevents invalid InnoDB definitions from being silently accepted.

Part 6 — Storage measurement with fio

Seeing "NVMe" and immediately writing innodb_io_capacity = 10000 is not a calculation. First determine where the MariaDB data directory lives:

SELECT @@datadir;

findmnt -T /var/lib/mysql
df -hT /var/lib/mysql
lsblk -d -o NAME,TRAN,ROTA,TYPE,SIZE,MODEL

Run fio on a separate test file — never on MariaDB data files

Do not run fio --filename=/var/lib/mysql/ibdata1 or point benchmarks at .ibd files.

sudo mkdir -p /var/lib/mysql/fio-test

sudo fio \
  --name=mariadb-storage \
  --filename=/var/lib/mysql/fio-test/testfile \
  --size=8G \
  --rw=randrw \
  --rwmixread=70 \
  --bs=16k \
  --ioengine=libaio \
  --direct=1 \
  --iodepth=32 \
  --numjobs=1 \
  --runtime=60 \
  --time_based \
  --group_reporting

Why 16 KiB? Traditional InnoDB uses 16 KiB pages by default, so --bs=16k approximates InnoDB-oriented storage testing. Real MariaDB I/O also includes sequential access, redo writes, fsync, background flushing, temporary files, and metadata — fio measures the storage subsystem, not MariaDB exactly.

Important fio parameters

--rw=randrw          Random read/write workload
--rwmixread=70       70% read, 30% write (adjust to your app: 90=read-heavy, 30=write-heavy)
--bs=16k             I/O size matching InnoDB page size
--iodepth=32         Outstanding I/O operations (queue depth)
--direct=1           Bypass OS page cache for the benchmark
--runtime=60         Run duration (use longer for serious benchmarking)

Do not look only at IOPS. 20,000 IOPS @ 0.5 ms is very different from 20,000 IOPS @ 20 ms. Also consider latency, bandwidth, queue depth, read/write mix, and tail latency.

Test different queue depths

for depth in 1 4 16 32 64; do
    fio \
      --name=mariadb-q${depth} \
      --filename=/var/lib/mysql/fio-test/testfile \
      --size=8G \
      --rw=randrw \
      --rwmixread=70 \
      --bs=16k \
      --ioengine=libaio \
      --direct=1 \
      --iodepth="$depth" \
      --numjobs=1 \
      --runtime=60 \
      --time_based \
      --group_reporting
done
Queue 1 → 3,000 IOPS → 0.3 ms
Queue 16 → 18,000 IOPS → 0.9 ms
Queue 32 → 22,000 IOPS → 1.7 ms
Queue 64 → 23,000 IOPS → 4.8 ms
Observation: additional 1,000 IOPS caused latency to jump from 1.7 ms to 4.8 ms — storage is approaching saturation.

Monitor with iostat and InnoDB status

iostat -xz 1

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_wait_free';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_flushed';

Is InnoDB keeping up with dirty-page flushing? If dirty pages keep growing while storage has headroom, background flushing may need attention. If storage latency is already high, increasing flushing can make the problem worse.

Part 7 — InnoDB I/O tuning

innodb_io_capacity

What it does: Controls I/O activity for InnoDB background work such as page flushing.

Default if omitted: 200

What goes wrong: Too low → dirty pages accumulate, checkpoint stalls, elevated latency. Too high → excessive flushing reduces cache effectiveness and increases storage latency.

How to calculate — do NOT set to maximum fio IOPS:

  1. Run fio to understand sustainable IOPS and latency at various queue depths.
  2. Start with a conservative value (e.g. 2,000–4,000 on capable NVMe).
  3. Run production-like load; monitor dirty pages and storage latency via iostat.
  4. Increase incrementally (e.g. 2,000 → 4,000 → 6,000) until dirty pages stabilize and latency remains acceptable.
  5. Stop when dirty pages are controlled, database latency is stable, and storage latency is acceptable.
fio reports ~22,000 IOPS → do not set innodb_io_capacity = 22000
Start at 4,000, test, increase to 6,000 when dirty pages grow under load.

innodb_io_capacity_max

What it does: Upper ceiling for background flushing during urgent situations.

Default if omitted: max(2000, 2 × innodb_io_capacity)

What goes wrong: Set too high relative to storage capability → pathological latency spikes during flush bursts.

How to calculate: Start with 2× innodb_io_capacity (e.g. 6,000 / 12,000). The 2× relationship is a starting point, not a law — validate against storage saturation from fio and iostat.

innodb_read_io_threads / innodb_write_io_threads: Default 4 each. Do not use CPU cores = I/O threads as a formula. Storage and workload matter more. Benchmark higher values only if I/O submission concurrency appears limiting.

innodb_flush_neighbors: On SSD/NVMe, innodb_flush_neighbors = 0 is commonly sensible — neighbor flushing optimizes for spinning disks. Validate on your exact version and storage.

innodb_lru_scan_depth: Default 1536 since MariaDB 10.5.7. No good universal formula. If flushing is healthy, leave it alone. If flushing is unhealthy, measure first — do not increase simply because the server uses NVMe.

Part 8 — Temporary tables and per-connection memory

tmp_table_size and max_heap_table_size

What it does: Limits in-memory internal temporary tables. Effective limit = MIN(tmp_table_size, max_heap_table_size).

Default if omitted: 16 MiB for both.

What goes wrong: Too small → disk-based temp tables (slower). Too large → potential memory exposure under concurrency (500 connections × 1G limit is not allocated to every session, but exposes dangerous ceiling).

How to calculate:

  1. Measure: SHOW GLOBAL STATUS LIKE 'Created_tmp%';
  2. Calculate disk temp table ratio: Created_tmp_disk_tables / Created_tmp_tables
  3. If high, investigate query causes (GROUP BY, ORDER BY, missing indexes) before raising limits.
  4. Start conservatively: 64M–128M. MariaDB historically recommends 32–64 MB and increasing after measurement.

sort_buffer_size

What it does: Per-session buffer for sort operations.

Default if omitted: 2 MiB

What goes wrong: Too large globally → 8M × 100 concurrent sorts = 800M just for that buffer class. Fix missing indexes before raising globally.

How to calculate: Check SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';, run EXPLAIN on slow queries. If a query performs a huge sort because an index is missing, fix the index — do not set sort_buffer_size = 64M globally.

join_buffer_size

What it does: Per-session buffer for joins that cannot use indexes effectively.

Default if omitted: 256 KiB

What goes wrong: Large global value → expensive when many connections perform joins concurrently. A large join buffer should make you ask: why isn't this join using an index?

How to calculate: Keep global value conservative. Optimize indexes and query plans. Use a larger session value only where justified.

read_buffer_size (default 128 KiB) and read_rnd_buffer_size (default 256 KiB) follow the same principle — per-session buffers multiply by concurrency. max_allowed_packet (default 16 MiB) and net_buffer_length (default 16 KiB) matter for large BLOB/text transfers. binlog_cache_size (default 32 KiB) — measure Binlog_cache_disk_use before increasing globally.

Part 9 — Connections and threading

max_connections

What it does: Maximum simultaneous client connections MariaDB will accept.

Default if omitted: 151 (minimum allowed: 10). MariaDB reserves one connection for privileged administrative access.

What goes wrong: Too low → "Too many connections" during peaks. Too high → allows more memory pressure and CPU contention without guaranteeing the server can handle that many active workloads.

How to calculate — from the application, not user count:

  1. Application Connections = Application Instances × Pool Size (e.g. 8 replicas × 15 = 120)
  2. Add administrative reserve and operational headroom (e.g. +10 +10 = 140)
  3. Validate: SHOW GLOBAL STATUS LIKE 'Max_used_connections';
  4. Apply safety factor: Target = Observed Peak × 1.25–1.5
Peak observed = 180 · Safety margin 25% → 180 × 1.25 = 225 → max_connections = 225

max_user_connections

What it does: Per-user connection limit.

Default if omitted: 0 (no global per-user limit)

What goes wrong: Without limits, one application account can consume all available connections.

How to calculate: Set slightly below max_connections for the main application account (e.g. max_connections=225, max_user_connections=210).

thread_cache_size (default 256): measure Threads_created / Connections. If thread creation is high relative to connection churn, a larger cache can help. Ignored when the thread pool is active.

thread_handling: Linux default is one-thread-per-connection. The MariaDB thread pool (available without commercial restriction) can reduce scheduling pressure at very high connection counts — benchmark first, do not enable just because "high concurrency = thread pool".

back_log: Automatic value is MIN(900, 50 + max_connections / 5). For max_connections=300 → back_log ≈ 110. Rarely needs manual tuning.

Part 10 — Table cache, file descriptors, and OS limits

table_open_cache

What it does: Number of open table handles cached.

Default if omitted: 2000

What goes wrong: Too small → frequent table open/close overhead (Opened_tables grows rapidly). Too large → wasted memory and file descriptors.

How to calculate: Check table count: SELECT COUNT(*) FROM information_schema.tables;. Set modestly above typical open table count. Verify with SHOW GLOBAL STATUS LIKE 'Opened_tables'; and Open_tables after real workload.

table_definition_cache (default 400): caches table definition metadata. Increase when many distinct tables are accessed. Default was raised in newer MariaDB versions — verify on your installation.

open_files_limit

What it does: Maximum number of files MariaDB can open.

Default if omitted: Platform-dependent; often 32,768 or similar on modern Linux.

What goes wrong: Too low → "Too many open files" errors. Setting high in MariaDB without raising OS limits has no effect.

How to calculate:

FD Requirement ≈ MAX(max_connections × 5, max_connections + table_open_cache × 2)

Verify OS limits:

ulimit -n
systemctl show mariadb --property=LimitNOFILE

systemd TasksMax and container ulimits matter at high connection counts. MariaDB cannot exceed the operating system's hard limit.

Part 11 — Timeouts, logging, and query optimization

Timeouts must be designed as a chain — application timeout, connection pool timeout, MariaDB wait_timeout, and max_statement_time should be coherent, not contradictory.

wait_timeout / interactive_timeout

What it does: Closes idle non-interactive / interactive connections after N seconds.

Default if omitted: 28800 seconds (8 hours)

What goes wrong: Too high → idle connections hold resources. Too low → connection pool reconnect storms.

How to calculate: Set slightly above your application's connection pool idle timeout. Common production values: 300–600 seconds for pooled applications.

connect_timeout (default 10s), net_read_timeout (default 30s), net_write_timeout (default 60s): align with application and load balancer timeouts.

max_statement_time (default 0 = unlimited): set a sane limit for runaway queries on supported versions.

Slow query logging:

slow_query_log = ON
long_query_time = 1

MariaDB 10.11 introduced log_slow_query_time as an alias for long_query_time.

Query optimization before memory optimization. The query cache was removed in MariaDB 10.1.5 — do not configure it. Performance Schema (enabled by default in modern MariaDB) helps identify expensive statements. Fix indexes and query plans before globally increasing per-session buffers.

Binary log format: ROW is the modern default and recommended for replication. Transaction isolation: understand your application's requirements — do not change globally without analysis. innodb_lock_wait_timeout (default 50s): tune based on application deadlock tolerance.

tmpdir: ensure adequate filesystem capacity for temporary files. Docker storage limits and volume sizing matter — a full tmpdir causes query failures unrelated to memory tuning.

Part 12 — Docker, Kubernetes, and container limits

Container memory limits are hard boundaries. MariaDB inside a 16 GB Docker container with mem_limit: 16g must be sized against 16 GiB — not the host's 128 GiB.

services:
  mariadb:
    image: mariadb:11.8
    mem_limit: 16g
    cpus: 4
    volumes:
      - ./conf.d:/etc/mysql/conf.d:ro
      - mariadb-data:/var/lib/mysql
    ulimits:
      nofile:
        soft: 65535
        hard: 65535

For Kubernetes, respect resources.limits.memory and resources.limits.cpu. MariaDB concurrency must be validated against container CPU limits. Storage class and volume performance determine I/O capacity — benchmark fio from inside the pod's volume mount when possible.

CPU limits can cause throttling that looks like database slowness. If pidstat shows MariaDB waiting on CPU while queries are "slow," check cgroup CPU limits before tuning InnoDB.

Part 13 — Worked example: 48 GiB shared VM

Assumptions:
VM RAM = 48 GiB · CPU = 8 vCPU · Storage = NVMe SSD · MariaDB = 11.8
Other services = 8 GiB · OS/operational reserve = 4 GiB

Step 1 — MariaDB memory budget

MariaDB budget = 48 − 8 − 4 = 36 GiB

Step 2 — Connection memory budget

Peak measured application connections = 180. Safety margin 25% → 180 × 1.25 = 225max_connections = 225.

Per-session buffers (conservative):

sort_buffer_size      = 2M
join_buffer_size      = 256K
read_buffer_size      = 128K
read_rnd_buffer_size  = 256K
# Sum ≈ 2.625M × 225 ≈ 591M ≈ 0.56 GiB

Reserve ~2 GiB for session/query/temporary activity.

Step 3 — Buffer pool

36 GiB budget
− 2 GiB connection/query
− 2 GiB global/internals
− 3 GiB operational safety
= 29 GiB → round to innodb_buffer_pool_size = 28G

We did not use 48 × 70%. We calculated it.

Step 4 — Redo

Peak production-like testing shows redo generation = 12 MiB/s. Target smoothing window = 30 minutes:

12 × 1800 = 21,600 MiB ≈ 21.1 GiB

Compare with 28 GiB buffer pool. This may reveal that the workload needs architectural attention — if the database genuinely generates 12 MiB/s continuously, understand why before merely allocating more disk.

Step 5 — I/O capacity

fio shows ~40,000 sustainable IOPS at 16 KiB mixed random, but latency acceptable only up to ~25,000 IOPS. Start innodb_io_capacity = 4000, test under load, increase to 6000 when dirty pages grow, set innodb_io_capacity_max = 12000 as ceiling. Not derived from max fio IOPS.

Step 6 — Table cache and file descriptors

2,800 tables → table_open_cache = 4000, table_definition_cache = 3000.

MAX(225 × 5, 225 + 4000 × 2) = MAX(1125, 8225) = 8225
→ open_files_limit = 65535 (if OS/container allows)

Part 14 — Example production my.cnf

Based on the 48 GiB example above. Assumptions: MariaDB 11.8 · 48 GiB RAM · 8 vCPU · NVMe · Shared VM · 28 GiB buffer pool · 225 max connections · 6,000 I/O capacity.

[mariadbd]

# ============================================================
# INNODB MEMORY
# ============================================================
# Calculated from MariaDB memory budget after reserving
# connections, temporary work, internal allocations, headroom.
innodb_buffer_pool_size = 28G

# ============================================================
# BUFFER POOL WARMUP
# ============================================================
innodb_buffer_pool_dump_at_shutdown = ON
innodb_buffer_pool_load_at_startup = ON

# ============================================================
# INNODB REDO
# ============================================================
innodb_log_file_size = 16G
innodb_log_buffer_size = 64M

# ============================================================
# DURABILITY
# ============================================================
innodb_flush_log_at_trx_commit = 1

# ============================================================
# INNODB I/O
# ============================================================
innodb_io_capacity = 6000
innodb_io_capacity_max = 12000
innodb_read_io_threads = 4
innodb_write_io_threads = 4
innodb_flush_neighbors = 0

# ============================================================
# TEMPORARY TABLES
# ============================================================
tmp_table_size = 128M
max_heap_table_size = 128M

# ============================================================
# PER-CONNECTION MEMORY
# ============================================================
sort_buffer_size = 2M
join_buffer_size = 256K
read_buffer_size = 128K
read_rnd_buffer_size = 256K
binlog_cache_size = 256K
net_buffer_length = 16K
max_allowed_packet = 64M

# ============================================================
# CONNECTIONS
# ============================================================
max_connections = 225
max_user_connections = 210
thread_cache_size = 64
back_log = 128

# ============================================================
# TABLE CACHE
# ============================================================
table_open_cache = 4000
table_definition_cache = 3000

# ============================================================
# FILE DESCRIPTORS
# ============================================================
open_files_limit = 65535

# ============================================================
# CONNECTION TIMEOUTS
# ============================================================
wait_timeout = 600
interactive_timeout = 600
connect_timeout = 10
net_read_timeout = 30
net_write_timeout = 60

# ============================================================
# SLOW QUERY LOG
# ============================================================
slow_query_log = ON
long_query_time = 1

# ============================================================
# TRANSACTIONAL SAFETY
# ============================================================
innodb_strict_mode = ON

Intentionally absent:

A good production my.cnf should not contain every variable you have ever heard of. It should express intentional deviations from MariaDB defaults, with comments explaining why each value exists.

Part 15 — Reference tables

Version changes — quick lookup

Version    │ Key changes relevant to tuning
───────────┼──────────────────────────────────────────────────────────────────
10.5       │ Single buffer-pool instance (instances deprecated)
           │ Single redo log file architecture begins
           │ Background I/O mechanism changes
10.6       │ Removed: innodb_buffer_pool_instances, innodb_log_files_in_group,
           │         innodb_page_cleaners
           │ Unix innodb_flush_method default = O_DIRECT
10.8       │ innodb_buffer_pool_chunk_size autosized
10.9       │ innodb_log_file_size dynamically resizable
10.11 LTS  │ Slow-query alias log_slow_query_time; active maintenance 2026
11.0       │ Deprecated: innodb_flush_method, innodb_file_per_table,
           │         innodb_doublewrite (granular I/O vars introduced)
11.4 LTS   │ Cost-based optimizer model for SSD characteristics; test plans after upgrade
11.8 LTS   │ Current Community LTS baseline; active maintenance 2026

Upstream defaults — quick lookup

Variable                          │ Default
──────────────────────────────────┼────────────────────────────
innodb_buffer_pool_size           │ 128 MiB
innodb_log_file_size              │ 96 MiB (10.5+); 48 MiB (10.4-era)
innodb_log_buffer_size            │ 16 MiB
innodb_flush_log_at_trx_commit    │ 1
innodb_io_capacity                │ 200
innodb_io_capacity_max            │ max(2000, 2 × io_capacity)
innodb_read_io_threads            │ 4
innodb_write_io_threads           │ 4
tmp_table_size                    │ 16 MiB
max_heap_table_size               │ 16 MiB
sort_buffer_size                  │ 2 MiB
join_buffer_size                  │ 256 KiB
read_buffer_size                  │ 128 KiB
read_rnd_buffer_size              │ 256 KiB
net_buffer_length                 │ 16 KiB
max_allowed_packet                │ 16 MiB
max_connections                   │ 151
thread_cache_size                 │ 256
table_open_cache                  │ 2000
table_definition_cache            │ 400
wait_timeout                      │ 28800 s
interactive_timeout               │ 28800 s
connect_timeout                   │ 10 s
net_read_timeout                  │ 30 s
net_write_timeout                 │ 60 s
long_query_time                   │ 10 s
max_statement_time                │ 0 (unlimited)

Verify actual values on your installation via information_schema.SYSTEM_VARIABLES — OS packages and Docker images may override upstream defaults.

Planning formulas

Category         │ Formula
─────────────────┼────────────────────────────────────────────────────────────
Memory budget    │ MariaDB Budget = RAM − Other Services − OS − Headroom
Buffer pool      │ Budget − Connection/Query − Temporary − Global/Internal
Physical reads   │ (buffer_pool_reads / read_requests) × 100
Redo capacity    │ Peak Redo Rate × Smoothing Window
                 │ Secondary: 25–50% of buffer pool
I/O capacity     │ Start conservative; iterate with fio + iostat + dirty pages
                 │ Never = max fio IOPS
Connections      │ Instances × Pool Size + Admin + Headroom
                 │ Validate: Max_used_connections × 1.25–1.5
Temp tables      │ MIN(tmp_table_size, max_heap_table_size)
File descriptors │ MAX(max_connections × 5, max_connections + table_open_cache × 2)

Post-change monitoring

Area             │ Commands / tools
─────────────────┼────────────────────────────────────────────────────────────
Connections      │ SHOW GLOBAL STATUS LIKE 'Threads%';
                 │ SHOW GLOBAL STATUS LIKE 'Max_used_connections';
Temporary tables │ SHOW GLOBAL STATUS LIKE 'Created_tmp%';
Sorting          │ SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
Table cache      │ SHOW GLOBAL STATUS LIKE 'Opened_tables';
                 │ SHOW GLOBAL STATUS LIKE 'Open_tables';
Buffer pool      │ SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
Redo             │ SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';
Binary log       │ SHOW GLOBAL STATUS LIKE 'Binlog_cache%';
OS               │ iostat -xz 1 · free -h · vmstat 1
                 │ pidstat -p $(pidof mariadbd) 1

If vmstat shows swap activity (si/so) under database load, do not immediately increase the buffer pool — the memory budget may be too aggressive.

Tuning checklist

Before tuning
  [ ] MariaDB version identified (10.11 / 11.4 / 11.8 LTS)
  [ ] Option-file path identified (mariadbd --print-defaults)
  [ ] VM/container memory and CPU limits identified
  [ ] Datadir and storage device identified
  [ ] Other services on host identified

Memory
  [ ] MariaDB memory budget calculated
  [ ] Buffer pool sized from budget (not RAM × 0.8)
  [ ] Connection memory estimated
  [ ] Temporary-memory budget considered
  [ ] Safety headroom reserved

I/O
  [ ] Storage tested with fio (separate test file, not ibdata1)
  [ ] Multiple queue depths tested; latency measured
  [ ] iostat monitored under load
  [ ] InnoDB dirty-page behavior measured
  [ ] io_capacity tuned iteratively (not set to max fio IOPS)

Connections
  [ ] Application pool size known
  [ ] Peak connections measured (Max_used_connections)
  [ ] max_connections calculated from app architecture
  [ ] max_user_connections considered

File descriptors
  [ ] table_open_cache sized and verified
  [ ] open_files_limit calculated
  [ ] systemd LimitNOFILE and container ulimits checked

Queries
  [ ] Slow query log enabled
  [ ] EXPLAIN used on slow queries
  [ ] Missing indexes investigated before raising buffers

Version compatibility
  [ ] Deprecated variables removed (buffer_pool_instances, log_files_in_group)
  [ ] Removed variables not present in my.cnf
  [ ] Major-version release notes reviewed

Anti-patterns to avoid

Anti-pattern                              │ Why it fails
──────────────────────────────────────────┼────────────────────────────────────
"Use 80% of RAM for buffer pool"          │ Ignores other services and per-connection memory
"Use 1000 connections for a powerful server"│ Capacity ≠ safe concurrency
"NVMe means innodb_io_capacity = 20000"   │ Must measure; fio max ≠ sustainable background I/O
"More sort_buffer is faster"              │ Fix indexes first; per-session buffers multiply
"1 GB tmp_table_size for WordPress"       │ Exposes dangerous memory ceiling under concurrency
"Copy entire 2018 my.cnf"                 │ Ignores version changes and removed variables
"Query cache makes SELECT faster"         │ Removed in MariaDB 10.1.5
"innodb_io_capacity = fio max IOPS"       │ Causes pathological flushing and latency

Part 16 — Practical walkthrough

Use this sequence when tuning a production or staging MariaDB instance. Do not change every variable at once.

Step 1 — Identify version and effective configuration

SELECT VERSION();
mariadbd --print-defaults
SELECT VARIABLE_NAME, GLOBAL_VALUE, DEFAULT_VALUE, GLOBAL_VALUE_ORIGIN
FROM information_schema.SYSTEM_VARIABLES
WHERE VARIABLE_NAME IN ('innodb_buffer_pool_size','max_connections','innodb_io_capacity');

Step 2 — Determine resource boundary

free -h
docker inspect mariadb --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}'
# or kubectl get pod mariadb-0 -o jsonpath='{.spec.containers[0].resources}'

Write down: total RAM available to MariaDB, CPU limit, and what else runs on the host.

Step 3 — Build memory budget and size buffer pool

# MariaDB budget = available RAM − other services − OS − headroom
# Buffer pool = budget − connection/query − temporary − internal headroom

SELECT ROUND(SUM(data_length + index_length)/1024/1024/1024,2) AS innodb_gb
FROM information_schema.tables WHERE engine='InnoDB';

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

Step 4 — Measure redo generation

# Snapshot Innodb_os_log_written, wait 10+ minutes during peak traffic, snapshot again
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';

Calculate peak redo rate. Size innodb_log_file_size from peak rate × smoothing window, cross-check against 25–50% of buffer pool.

Step 5 — Benchmark storage with fio

sudo mkdir -p /var/lib/mysql/fio-test
sudo fio --name=test --filename=/var/lib/mysql/fio-test/testfile \
  --size=8G --rw=randrw --rwmixread=70 --bs=16k --ioengine=libaio \
  --direct=1 --iodepth=32 --runtime=60 --time_based --group_reporting

iostat -xz 1

Record sustainable IOPS and latency — not just peak IOPS.

Step 6 — Calculate max_connections from application

# App instances × pool size + admin + headroom
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';

Step 7 — Write version-controlled 99-production-tuning.cnf

# Example comment style:
# 2026-08-28 — Peak measured redo: 12 MiB/s; smoothing window ~20 min
# Validated under staging workload.
innodb_log_file_size = 16G

Mount via Docker Compose or place in mariadb.conf.d/. Restart MariaDB.

Step 8 — Load test and observe

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';

iostat -xz 1
vmstat 1
pidstat -p $(pidof mariadbd) 1

Change one category at a time. Compare against baseline. Keep or rollback.

Step 9 — Iterate io_capacity

Start conservative (e.g. 4000). Under production-like load, if dirty pages grow continuously and storage latency is acceptable, increase incrementally. Stop when dirty pages stabilize.

Part 17 — Tuning process and rollout

A tuning change is not finished when MariaDB starts successfully. Test with workload that resembles production — sysbench, application replay, API load test. Important metrics: QPS/TPS, p50/p95/p99 latency, CPU, RAM, swap, IOPS, I/O latency, Threads_running, buffer-pool reads, temporary disk tables, redo generation.

A configuration that improves average throughput but makes p99 terrible may be a regression.

Production rollout sequence:

Current production
      ↓
Create version-controlled candidate (Git + PR)
      ↓
Staging + production-like load test
      ↓
Canary / small deployment
      ↓
Observe (Part 15 monitoring table)
      ↓
Full rollout

For MariaDB version upgrades, a staged replica-based approach gives a safer rollback path. Review release notes for deprecated/removed variables before promoting.

Document why each value exists in version control:

# innodb_buffer_pool_size = 28G
# because: MariaDB memory budget is 36G, 8G reserved for
# connections, temporary work, internal memory, and safety.

Part 18 — Wrapping up

MariaDB tuning is not a collection of magic numbers. It is resource engineering.

RAM        → Memory budget     → Buffer pool + dynamic memory
Storage    → fio + iostat      → InnoDB flushing → io_capacity
Application→ Connection pool   → max_connections
Queries    → EXPLAIN + status  → Per-query buffers
Version    → Supported vars    → Current defaults → No deprecated options

The correct answer to "What is the best MariaDB configuration?" is almost never a configuration file. The better answer: first measure the environment, then calculate the configuration.

A production-ready MariaDB deployment should have enough memory to keep its working set hot, enough redo capacity to absorb the write workload, enough I/O capacity to keep dirty pages under control, enough connection capacity to satisfy the application, and enough operating-system headroom to remain stable during peaks. It should also remain understandable six months later.

The configuration should be measured, calculated, version-aware, tested, and documented — not copied, tweaked, and hoped.