Investigating MariaDB high memory usage requires more than comparing mariadbd RSS with innodb_buffer_pool_size. A production database intentionally keeps hot pages in memory. The process also maintains connection structures, sort and join workspaces, table caches, Performance Schema, replication state, plugins, temporary tables, and allocator arenas. Linux adds filesystem cache, shared pages, cgroups, tmpfs, and memory reclaim. One number cannot reconcile all of these layers perfectly.
An OOM kill is clearer in outcome but not automatically in cause. The kernel or a container cgroup may select MariaDB because it has the largest score, while a colocated backup process caused the final pressure. A pod can be killed at a 4 GiB limit even though the Kubernetes node has free RAM. Restarting with the same configuration and workload can create a crash-recovery loop, while immediately shrinking the buffer pool can trade memory pressure for severe storage latency.
This guide builds a safe workflow from host and cgroup evidence to MariaDB allocation sources. It explains RSS, anonymous memory, page cache, global caches, on-demand session buffers, temporary work, instrumentation limits, live mitigation, buffer-pool resizing, OOM recovery, and a concurrency-based production budget. The objective is stable headroom and predictable failure behavior, not the lowest possible memory graph.
Decide whether the memory is healthy, pressured, or leaking
Classify the symptom:
- Healthy cache: RSS rises during warm-up, then stabilizes; swap and pressure stay low; latency improves.
- Workload burst: memory tracks reports, imports, connection count, or concurrency and later falls partly or fully.
- Allocator retention: MariaDB frees objects internally but the allocator does not immediately return pages to the OS; RSS remains high but reusable memory exists.
- Unbounded configuration: global caches plus concurrent private work exceed host or cgroup capacity.
- Leak or defect: memory grows across comparable workload cycles without a corresponding tracked allocation or reuse plateau.
- External pressure: another process, tmpfs, kernel memory, sidecar, or page cache consumes the missing capacity.
- OOM kill: the kernel or cgroup terminates a process after reclaim cannot satisfy an allocation under its policy.
Do not call every stable high RSS a leak. A database reserving memory for repeated use is often efficient. Require a time series, comparable workload, allocation evidence, and release-specific defect research before making that claim.
Define the service limits:
- Physical and usable RAM.
- Swap policy and current use.
- Container/cgroup hard and soft limits.
- MariaDB workload baseline and peak.
- Required failover headroom.
- Acceptable query rejection versus process termination.
- Recovery-time objective after an OOM.
Preserve OOM evidence before it scrolls away
After a host-level OOM, inspect kernel logs:
journalctl -k --since '-2 hours' --no-pager
dmesg --ctime | tail -n 300
Look for Out of memory, oom-kill, Killed process, selected PID, memory cgroup, task RSS, page-table use, and constraint type. Kernel formats vary. Save the complete event with UTC mapping instead of copying one “Killed process” line.
Inspect the MariaDB unit around the event:
journalctl -u mariadb --since '-2 hours' --no-pager
systemctl status mariadb --no-pager
Determine whether systemd reports an OOM result, a signal, an application crash, or an administrator stop. A process exit code alone may not show who invoked the OOM killer.
On a cgroup v2 system:
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.high
cat /sys/fs/cgroup/memory.events
cat /sys/fs/cgroup/memory.stat
These paths apply only when the shell is in the relevant cgroup. For a systemd service, find its control group:
systemctl show mariadb --property=ControlGroup --value
systemctl show mariadb --property=MemoryCurrent --property=MemoryMax
Then inspect the correct cgroup path or use systemd metrics. In Kubernetes, use pod/container memory working set, limit, OOM termination reason, restart count, and node events. Node free memory does not override a container limit.
memory.events counters such as high, max, oom, and oom_kill are cumulative. Capture interval changes and the effective hierarchy; a parent cgroup can impose the binding limit.
Confirm which process and resource grew
Identify all MariaDB instances and related tools:
ps -eo pid,ppid,user,stat,etimes,rss,vsz,pmem,pcpu,comm,args
--sort=-rss | head -n 40
Map the service PID:
systemctl show mariadb --property=MainPID --value
ps -p "$(systemctl show mariadb --property=MainPID --value)"
-o pid,ppid,user,etimes,rss,vsz,pmem,pcpu,args
Do not confuse:
- VSZ/VIRT: virtual address space mapped or reserved; not resident RAM.
- RSS/RES: resident pages attributed to the process, including some shared mappings.
- PSS: shared pages divided among processes, often a better proportional view.
- Anonymous RSS: heap, stacks, and private mappings.
- File-backed RSS: executable, libraries, mmap, or file pages.
- Swap: process pages moved to swap.
Read a stable kernel summary:
pid="$(systemctl show mariadb --property=MainPID --value)"
cat "/proc/$pid/status"
cat "/proc/$pid/smaps_rollup"
smaps_rollup availability and permissions vary. Avoid repeatedly scanning full smaps for a very large process during an incident; it adds overhead.
Sample trends:
pidstat -r -p "$pid" 1 10
Check the whole host:
free -h
vmstat 1 10
cat /proc/meminfo
cat /proc/pressure/memory
Linux uses otherwise idle memory for cache. The available estimate is more useful than free alone. Sustained reclaim, swap-in/out, major faults, or memory pressure is stronger evidence than a low free number.
Check colocated and transient consumers
MariaDB may not be the only cause. Inspect:
mariadb-backupand compression processes.- Logical dump clients and pipelines.
- Monitoring, antivirus, security, or log agents.
- Application and proxy sidecars.
- Page cache from large backup or scan activity.
- tmpfs mounts and shared memory.
- Other database instances.
- Kernel slab and network buffers.
ps -eo pid,ppid,user,rss,vsz,pmem,pcpu,comm,args --sort=-rss | head -n 50
findmnt -t tmpfs
df -hT -t tmpfs
df on tmpfs reports filesystem usage, but pages may also be accounted to a cgroup. MariaDB tmpdir on tmpfs turns disk-style spills into memory consumption and can trigger a container OOM.
Check slab at the host level where permitted:
slabtop -o 2>/dev/null | head -n 30
Do not drop Linux caches to “free memory” on production. echo 3 > /proc/sys/vm/drop_caches destroys useful cache, adds I/O, and does not fix an overcommitted configuration.
Capture MariaDB version and memory controls
Query the actual server:
SELECT VERSION(), @@version_comment, NOW(6), @@GLOBAL.server_id;
SHOW GLOBAL VARIABLES WHERE Variable_name IN (
'innodb_buffer_pool_size',
'key_buffer_size',
'aria_pagecache_buffer_size',
'max_connections',
'thread_handling',
'performance_schema',
'max_session_mem_used',
'tmp_table_size',
'max_heap_table_size',
'sort_buffer_size',
'join_buffer_size',
'read_buffer_size',
'read_rnd_buffer_size',
'binlog_cache_size',
'max_binlog_cache_size',
'table_open_cache',
'table_definition_cache'
);
Not every variable exists on every release. Values are limits or settings with different allocation timing. Adding them mechanically and multiplying all session values by max_connections produces an unrealistic but useful worst-case warning, not an actual forecast.
Capture connections and memory-related status:
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Threads_connected',
'Threads_running',
'Max_used_connections',
'Memory_used',
'Memory_used_initial',
'Created_tmp_tables',
'Created_tmp_disk_tables',
'Prepared_stmt_count',
'Open_tables',
'Open_table_definitions',
'Uptime'
);
Memory_used is version-sensitive. MariaDB documentation states that from 10.6.16 it includes global allocations such as the InnoDB buffer pool and key buffer; older output excluded such global areas. Memory_used_initial is introduced only in newer release lines. Never compare the counter across versions without normalizing semantics.
Build a realistic MariaDB memory model
Use categories instead of one formula.
Global or instance-wide allocations
These include:
- InnoDB buffer pool and associated structures.
- InnoDB redo/log buffers and adaptive structures.
- Aria page cache and MyISAM key cache where used.
- Performance Schema tables and buffers.
- Table definition/open table caches.
- Query cache on old supported configurations.
- Replication, Galera, audit, encryption, and plugin state.
- Server dictionaries and internal caches.
Per-connection baseline
Each connection has thread/session objects, network buffers, diagnostics, prepared statements, transaction state, and authentication context. An idle connection is not free, but usually consumes less than an active complex query.
Per-operation or on-demand workspace
A running statement can allocate:
- Sort buffers, potentially for multiple sort operations.
- Join buffers for joins lacking direct indexed access.
- Read and random-read buffers.
- Internal temporary tables.
- Binlog transaction caches.
- Stored routine and expression state.
- Transaction/undo-related in-memory structures.
These buffers are commonly allocated when required, not all at connection creation. One statement can use more than one operation of a category. Therefore:
memory_peak ≠ global_buffers + max_connections * one_simple_sum
A practical scenario model is:
expected_peak = global_baseline
+ idle_connections * measured_idle_session
+ active_light_sessions * measured_light_workspace
+ active_heavy_sessions * measured_heavy_workspace
+ backup_replication_plugin_peak
+ OS_and_sidecar_reserve
+ safety_margin
Measure concurrency distributions, not only Max_used_connections. Ten simultaneous report queries can consume more private memory than 1,000 idle pooled connections.
Size the InnoDB buffer pool in context
The buffer pool is usually the largest intentional allocation on an InnoDB-focused host:
SELECT @@GLOBAL.innodb_buffer_pool_size / 1024 / 1024 / 1024
AS buffer_pool_gib;
Official documentation notes that a dedicated InnoDB server may use up to roughly 80% of total memory, but this is not a universal target. Containers, backups, high connection concurrency, Galera, other engines, large sorts, and colocated services require more reserve.
Inspect pool content and pressure:
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Innodb_buffer_pool_pages_total',
'Innodb_buffer_pool_pages_data',
'Innodb_buffer_pool_pages_dirty',
'Innodb_buffer_pool_pages_free',
'Innodb_buffer_pool_reads',
'Innodb_buffer_pool_read_requests',
'Innodb_buffer_pool_wait_free',
'Innodb_buffer_pool_resize_status'
);
Use interval rates. A buffer pool with few free pages is normal because it is a cache. High physical read rates, storage latency, and Innodb_buffer_pool_wait_free matter more than free-page count alone.
Shrink the buffer pool only with an impact plan
innodb_buffer_pool_size is dynamic on modern MariaDB releases:
SET GLOBAL innodb_buffer_pool_size = 12 * 1024 * 1024 * 1024;
The 12 GiB value is an example, not a recommendation. Shrinking evicts cached pages and may require dirty-page progress, increasing I/O and latency. It can take time.
Monitor:
SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_resize_status';
SHOW ENGINE INNODB STATUSG
Persist an approved target in managed configuration:
[mariadb]
innodb_buffer_pool_size = 12G
Verify option precedence with mariadbd --print-defaults. A runtime shrink without a persistent fix can return to the unsafe value after restart.
Recent maintenance releases in the 10.11, 11.4, and 11.8 families add Linux memory-pressure auto-shrink controls such as innodb_buffer_pool_size_auto_min and innodb_buffer_pool_size_max. Feature-detect them:
SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size_auto_min';
SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size_max';
Do not assume every 10.11 installation has this backport; patch level matters. Auto-shrink is a guardrail, not a substitute for an adequate budget, and shrinking under pressure can expose the storage system to a read surge.
Find connection-driven memory growth
Track global and active counts:
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Threads_connected',
'Threads_running',
'Max_used_connections',
'Connections',
'Aborted_clients'
);
List current sessions:
SELECT ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
LEFT(INFO, 500) AS sql_text
FROM information_schema.PROCESSLIST
ORDER BY COMMAND = 'Sleep', TIME DESC;
Look for:
- Pool multiplication across application replicas.
- Idle sessions with prepared statements or open transactions.
- Heavy queries running concurrently.
- Connection leaks and retry storms.
- Administrative or report users with oversized session buffers.
- Replication and event threads.
Reducing max_connections does not disconnect existing sessions and does not fix pool design. Set application pool maximums, acquisition timeouts, idle retirement, and total fleet budget. Reserve tested administrative capacity.
Thread pooling can reduce active thread structures and context switching, but it does not remove session objects or query workspaces. Test it against the exact workload.
Audit session buffer settings
Compare global defaults and values on the real application connection:
SELECT @@GLOBAL.sort_buffer_size,
@@SESSION.sort_buffer_size,
@@GLOBAL.join_buffer_size,
@@SESSION.join_buffer_size,
@@GLOBAL.read_buffer_size,
@@SESSION.read_buffer_size,
@@GLOBAL.read_rnd_buffer_size,
@@SESSION.read_rnd_buffer_size,
@@GLOBAL.tmp_table_size,
@@SESSION.tmp_table_size,
@@GLOBAL.max_heap_table_size,
@@SESSION.max_heap_table_size;
Application startup hooks can override session values. Raising global buffers to make one report faster increases exposure for every new connection. Prefer query/index fixes and, if justified, a dedicated report pool with measured session settings.
tmp_table_size and max_heap_table_size use the lower session value as the effective internal in-memory temporary-table ceiling. This is a per-operation limit, not memory reserved up front. Large limits under concurrent aggregation can still create a dangerous peak.
Join buffers are not a replacement for indexes. A multi-join statement can need multiple buffers. Sort buffer size can apply to sort operations, and bigger is not always faster for small sorts.
Inspect explicit MEMORY tables
User-created MEMORY tables hold data in RAM and are constrained per table by the relevant max_heap_table_size at creation or alteration time.
Inventory them:
SELECT TABLE_SCHEMA,
TABLE_NAME,
TABLE_ROWS,
DATA_LENGTH,
INDEX_LENGTH
FROM information_schema.TABLES
WHERE ENGINE = 'MEMORY'
ORDER BY DATA_LENGTH + INDEX_LENGTH DESC;
InnoDB row estimates and MEMORY size metadata have limitations, but this provides a starting list.
MEMORY data disappears on restart while table definitions remain. Do not use it as durable storage. For large or unpredictable working sets, an explicit InnoDB temporary table often provides safer capacity behavior than forcing everything into RAM.
Dropping or truncating a MEMORY table destroys its contents. Confirm ownership and reconstruction before any incident action.
Use Performance Schema memory instrumentation
MariaDB 10.5.2+ includes memory summary tables where Performance Schema and memory instruments are available:
memory_summary_global_by_event_namememory_summary_by_thread_by_event_namememory_summary_by_account_by_event_namememory_summary_by_user_by_event_namememory_summary_by_host_by_event_name
Check availability:
SELECT @@performance_schema;
SHOW TABLES FROM performance_schema LIKE 'memory%';
SELECT NAME, ENABLED
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'memory/%'
LIMIT 100;
Rank currently tracked global allocations:
SELECT EVENT_NAME,
CURRENT_COUNT_USED,
CURRENT_NUMBER_OF_BYTES_USED,
HIGH_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_global_by_event_name
WHERE CURRENT_NUMBER_OF_BYTES_USED > 0
ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC
LIMIT 30;
Rank foreground threads:
SELECT m.THREAD_ID,
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
SUM(m.CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
SUM(m.HIGH_NUMBER_OF_BYTES_USED) AS high_bytes
FROM performance_schema.memory_summary_by_thread_by_event_name AS m
LEFT JOIN performance_schema.threads AS t
ON t.THREAD_ID = m.THREAD_ID
GROUP BY m.THREAD_ID,
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST
ORDER BY current_bytes DESC
LIMIT 30;
These tables show instrumented allocations, not every byte in RSS. Instruments can be disabled, allocator overhead/fragmentation is external, shared libraries and stacks may not reconcile, and version coverage evolves. Use them for attribution and trends, not a perfect accounting ledger.
Performance Schema itself uses memory. Inspect its internal table/buffer use:
SHOW ENGINE PERFORMANCE_SCHEMA STATUS;
Do not enable every consumer and oversized history table during an OOM incident. Observability needs its own budget.
Use Memory_used and per-session limits carefully
Where supported, query global and session status:
SHOW GLOBAL STATUS LIKE 'Memory_used%';
SHOW SESSION STATUS LIKE 'Memory_used%';
On modern releases, session Memory_used tracks allocations attributed to that connection. The global semantic changed across maintenance versions, so record the exact server version.
MariaDB exposes max_session_mem_used on supported releases:
SHOW GLOBAL VARIABLES LIKE 'max_session_mem_used';
SELECT @@SESSION.max_session_mem_used;
It limits memory a user session is allowed to allocate according to MariaDB's tracked Memory_used. Test the exact error behavior, exemptions, and coverage before using it as a hard guardrail. It cannot cap untracked allocator overhead or global caches and does not replace workload admission control.
Apply a restrictive session limit first to a dedicated report user/connection in staging. Ensure the application fails the query cleanly, rolls back transactions, and does not retry endlessly.
Correlate memory with query digests
Digest summaries do not directly report peak memory per query on every release, but they identify likely causes:
SELECT SCHEMA_NAME,
DIGEST,
COUNT_STAR,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT,
SUM_CREATED_TMP_TABLES,
SUM_CREATED_TMP_DISK_TABLES,
SUM_SORT_ROWS,
LEFT(DIGEST_TEXT, 500) AS digest_text
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST IS NOT NULL
ORDER BY SUM_CREATED_TMP_TABLES DESC, SUM_SORT_ROWS DESC
LIMIT 30;
Inspect the actual active thread's memory summary while a known query runs in a safe test. Correlate concurrency, not just one statement's peak.
Common query patterns include:
- Large GROUP BY or DISTINCT results.
- Wide sorts and multiple filesorts.
- Joins without selective indexes.
- Huge
INlists or dynamically generated SQL. - Large transactions and binlog caches.
- Explicit temporary MEMORY tables.
- Stored routines building large intermediate state.
- Clients requesting enormous results.
Fix rows and row width first. Increasing a buffer can move a spill into RAM and worsen the OOM risk.
Investigate prepared statements and caches
Check prepared statement count:
SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';
SHOW GLOBAL VARIABLES LIKE 'max_prepared_stmt_count';
A steadily growing count aligned with connection lifetime can indicate applications do not close prepared statements. Inspect performance_schema.prepared_statements_instances where available:
DESCRIBE performance_schema.prepared_statements_instances;
SELECT COUNT(*) FROM performance_schema.prepared_statements_instances;
Table caches also use memory:
SHOW GLOBAL VARIABLES WHERE Variable_name IN
('table_open_cache','table_definition_cache','table_open_cache_instances');
SHOW GLOBAL STATUS WHERE Variable_name IN
('Open_tables','Opened_tables','Open_table_definitions','Opened_table_definitions');
Large caches are not automatically leaks. Compare workload, schema count, misses, file-descriptor limits, and memory cost. Do not shrink them abruptly and create metadata churn without testing.
Handle a live memory-pressure incident
1. Stop amplification
Pause optional reports, imports, schema builds, backup compression, and retry storms. Reduce application concurrency before the kernel does it indiscriminately.
2. Preserve evidence
Capture process/cgroup memory, kernel logs, MariaDB variables/status, active queries, thread memory, connection counts, and recent deployment/job timeline.
3. Cancel the narrowest high-memory statement
After verifying ownership:
KILL QUERY 12345;
This stops the current statement but does not necessarily close an open transaction or prevent immediate retry. Coordinate with the application.
KILL CONNECTION 12345;
closes the session and rolls back its transaction. A large rollback can consume memory, CPU, and I/O. Do not kill based only on connection age.
4. Reduce dynamic global memory if the trade-off is acceptable
A controlled buffer-pool shrink can restore headroom, but monitor resize, dirty pages, storage latency, and service P99. Do not shrink below a tested floor in one jump.
5. Adjust the cgroup only with node capacity evidence
Raising a container limit can stop imminent OOM if the node has reserved headroom. It can also transfer the failure to the node or another pod. Update requests, limits, scheduling, and failover capacity together.
6. Avoid dangerous “free memory” actions
Do not drop caches, disable swap abruptly, truncate unknown tables, delete temporary files, or restart repeatedly. These actions can increase pressure or lose evidence/data.
Recover after MariaDB was OOM-killed
An OOM kill is an unclean process termination. InnoDB should perform crash recovery on restart using redo, but recovery needs memory and I/O.
Before automatic restart loops consume the host:
- Preserve kernel and service logs.
- Confirm available memory/cgroup limit and competing processes.
- Stop the workload from reconnecting aggressively.
- Correct the unsafe startup memory setting or limit.
- Start MariaDB once under observation.
- Monitor crash recovery, disk, memory, and logs.
- Verify schemas, writes, replication/cluster state, and application checks.
systemctl reset-failed mariadb
systemctl start mariadb
journalctl -u mariadb -f
Run these only after the capacity issue is addressed. journalctl -f remains attached until interrupted.
Do not delete ib_logfile0, temporary-looking InnoDB files, or data files to make startup proceed. That can destroy recovery information.
After start:
SELECT VERSION(), NOW(6);
SHOW ENGINE INNODB STATUSG
SHOW ALL REPLICAS STATUSG
Use topology-appropriate commands. Validate actual read/write paths and recovery objectives; a running PID is not proof the service is healthy.
Configure swap deliberately
Swap is not a substitute for RAM, but a small controlled amount can provide the kernel time to reclaim or let operators mitigate a transient spike. Heavy database swapping causes severe latency.
Inspect:
swapon --show
sysctl vm.swappiness
vmstat 1 10
Do not run swapoff -a during memory pressure; moving swapped pages back to RAM can trigger the OOM you are trying to avoid. Change swap and swappiness only through host policy and load testing.
In Kubernetes, swap availability depends on node/runtime policy and is often disabled. Do not assume a container has host swap protection.
Build a production memory budget
Start from the binding limit, not physical RAM alone:
usable_limit = min(host_policy_limit, cgroup_hierarchy_limit)
Reserve memory for:
- Kernel, agents, SSH/administration, and filesystem needs.
- Sidecars and colocated services.
- Backup and upgrade peaks.
- Replication/Galera/plugin state.
- Crash recovery and failover traffic.
- Measurement error and allocator fragmentation.
Then budget MariaDB:
MariaDB budget = buffer pool
+ other global caches
+ idle session baseline
+ concurrent active workspaces
+ temporary/MEMORY table peak
+ replication/plugin peak
+ Performance Schema
+ safety margin
Use P95 and tested worst-case concurrency scenarios, not average workload. Include one or more deliberately bad-but-allowed queries until guardrails reject them.
Do not set the buffer pool to 80% of a pod limit and then budget nothing for connections, sidecars, backup, and kernel accounting. The percentage guidance assumes a primarily InnoDB dedicated server, not every deployment.
Roll out lasting corrections
Potential fixes include:
- Right-size and persist the buffer pool.
- Reduce total application pool capacity across replicas.
- Cap concurrent reports and batch jobs.
- Use query/index changes to reduce sort, join, and temp work.
- Move unpredictable analytics to isolated resources.
- Close prepared statements and leaked connections.
- Replace large MEMORY tables with safer storage.
- Move
tmpdiroff tmpfs when memory failure is unacceptable. - Size Performance Schema histories intentionally.
- Add per-session limits on supported versions after testing.
- Schedule backup compression away from peak workload.
- Upgrade when evidence points to a fixed MariaDB memory defect.
Change one dimension at a time where feasible. Record runtime and persistent settings, restart requirements, success metrics, and rollback.
For a suspected leak, reproduce with a supported current patch release, stable query workload, allocator/RSS/time series, and Performance Schema deltas. Search official changelogs and issue trackers for the exact branch. An upgrade is a software change requiring backup, compatibility, replication, and rollback tests.
Verify that the fix is real
Compare before and after:
| Evidence | Before | After |
|---|---|---|
| Process RSS/PSS/anonymous | Captured | Compared |
| Cgroup current/peak/events | Captured | Compared |
| Host available/swap/pressure | Captured | Compared |
MariaDB Memory_used semantics |
Versioned | Same version/context |
| Buffer pool and global caches | Captured | Compared |
| Connected/running sessions | Captured | Compared |
| Per-thread memory peaks | Captured | Compared |
| Temp/sort/query concurrency | Captured | Compared |
| Service P95/P99/errors | Captured | Compared |
| Storage latency and reads | Captured | Compared |
| OOM/restart counters | Captured | Zero new events |
Run normal, busy-hour, report, backup, failover, and recovery scenarios. A lower RSS that causes a storage-read storm and misses latency objectives is not a complete fix.
Observe at least one full business cycle. Allocator retention can make RSS plateau at the previous peak; demonstrate that it stops growing under repeated comparable cycles and that the cgroup retains safe headroom.
Troubleshoot common misleading cases
RSS is larger than innodb_buffer_pool_size
That is expected. RSS includes other global caches, session memory, stacks, allocator overhead, code, mappings, and instrumented/uninstrumented state. Build a category budget.
Linux free memory is near zero
Linux uses free RAM for cache. Inspect MemAvailable, swap activity, pressure, reclaim, and cgroup headroom before declaring an incident.
Memory remains high after a query finishes
MariaDB or the allocator may retain memory for reuse. Check Performance Schema current bytes, active threads, repeated-cycle plateau, and pressure rather than expecting immediate RSS return.
Memory spikes with many connections
Determine active versus idle sessions and heavy-query concurrency. Pool multiplication, large session defaults, prepared statements, and open transactions are common causes.
The pod OOMs while the node has free RAM
The container hit its cgroup limit. Inspect memory.max, events, pod limit, tmpfs, and sidecars. Node free memory cannot be borrowed past a hard container limit.
Reducing tmp_table_size did not fix the peak
The workload may use explicit MEMORY tables, sort/join buffers, binlog caches, large results, or uninstrumented/plugin memory. Attribute by thread and query instead of repeatedly changing one limit.
MariaDB restarts repeatedly after OOM
The startup buffer pool, recovery, or reconnect storm still exceeds available memory. Stop the loop, reserve headroom, lower persistent settings safely, control clients, then start once under observation.
Performance Schema totals do not match RSS
Coverage is incomplete by design. Allocator metadata/fragmentation, stacks, mappings, shared pages, plugins, and disabled instruments create gaps. Use it for attribution, not exact reconciliation.
Monitoring and alert design
Monitor:
- Process RSS, PSS where available, anonymous and swap.
- Cgroup current, high/max events, OOM, OOM kill, and peak.
- Host available memory, swap-in/out, reclaim, and PSI.
- Buffer pool configured size, data/free/dirty pages, resize status.
Memory_usedwith server-version semantics.- Per-thread/account/global instrumented memory.
- Connected/running sessions and pool totals.
- Prepared statements, MEMORY table size, temp/sort rates.
- Backup, report, DDL, and batch concurrency.
- Service latency/errors and storage reads after memory changes.
- Restart and crash-recovery duration.
Alert before the hard limit, using sustained growth, pressure, and forecast. An RSS threshold alone is noisy for a database cache. Pair memory headroom with workload and service impact.
Best practices checklist
- Preserve kernel and cgroup OOM evidence immediately.
- Confirm the binding host/container limit.
- Separate RSS, VSZ, PSS, anonymous, file-backed, and swap.
- Inventory colocated and tmpfs consumers.
- Version-gate MariaDB memory counters and controls.
- Budget global caches and concurrent on-demand work separately.
- Use realistic active concurrency, not only
max_connections. - Keep report-specific buffers out of global defaults.
- Treat Performance Schema as partial attribution.
- Shrink the buffer pool gradually with storage monitoring.
- Persist runtime changes before the next restart.
- Stop retry storms and optional jobs during pressure.
- Avoid drop-caches, swapoff, and blind restart loops.
- Validate crash recovery after an OOM kill.
- Test normal, peak, backup, and failover memory scenarios.
FAQ
Why does MariaDB use almost all available memory?
The InnoDB buffer pool intentionally caches data, and Linux uses memory for filesystem cache. High stable usage is normal when pressure, swap, latency, and cgroup headroom remain healthy.
Is RSS the same as MariaDB allocated memory?
No. RSS includes resident anonymous, file-backed, and shared pages, while MariaDB counters cover version-dependent tracked allocations. Allocator retention and instrumentation gaps prevent exact equality.
Can max_connections cause an OOM?
It defines possible concurrency, but actual risk depends on idle baseline, active session buffers, query operations, and pool behavior. A high value combined with heavy simultaneous queries is dangerous.
Should I reduce innodb_buffer_pool_size after an OOM?
Often it is part of restoring headroom, but shrink gradually and monitor storage reads, dirty pages, resize progress, and latency. Fix competing and per-session consumers too.
Does max_session_mem_used prevent every session OOM?
No. It limits memory tracked for a session on supported releases. It does not cover global caches, every plugin/allocator byte, or aggregate concurrency, so it is only one guardrail.
Why was the MariaDB pod OOM-killed with free node RAM?
The pod/container likely reached its cgroup memory limit. Inspect memory events, limit hierarchy, sidecars, tmpfs, and working set rather than host free RAM alone.
Is swap bad for MariaDB?
Heavy active swapping causes severe latency, but a controlled reserve can prevent abrupt OOM during a short spike. Define it with host policy; never disable swap during pressure casually.
Does a high RSS after workload ends prove a leak?
No. MariaDB and its allocator may retain freed memory for reuse. A leak requires repeated comparable-cycle growth and allocation evidence, ideally reproduced on a supported patch release.
Conclusion
Resolving MariaDB high memory usage begins at the boundary that can kill the process: the host or cgroup. Preserve OOM evidence, identify the effective limit, separate process memory from page cache and colocated consumers, and measure pressure over time. Then decompose MariaDB into global caches, idle sessions, concurrent query workspaces, temporary structures, replication/plugins, and instrumentation.
During an incident, stop workload amplification, cancel only verified harmful work, and shrink dynamic memory with an explicit storage-latency trade-off. Long-term stability comes from a concurrency-based budget, right-sized buffer pool, bounded pools and reports, query/index fixes, tested session guardrails, and monitored headroom. The correct target is not minimal RSS; it is maximum useful cache while every production and recovery scenario stays below a predictable safe limit.
Suggested Internal Links
- Troubleshoot MariaDB High CPU Usage Step by Step Safely
- Size the MariaDB InnoDB Buffer Pool for Production
- Configure MariaDB Connections and Thread Handling
- Tune MariaDB Temporary Tables and Sort Operations Safely
- Optimize MariaDB Redo Logs and Transaction Durability
- Use the MariaDB Slow Query Log for Better Diagnosis