MySQL Configuration
MySQL's behavior is controlled by its configuration file and runtime system variables. Understanding the key settings and how to tune them for your workload is essential for running a healthy production database. The wrong defaults — especially for the InnoDB buffer pool — can make a well-designed schema perform poorly.
Configuration File Locations
MySQL reads configuration from my.cnf (Linux/macOS) or my.ini (Windows). Multiple locations are checked in order — settings in later files override earlier ones:
Platform | File Search Order |
|---|---|
Linux (system-wide) | /etc/my.cnf, /etc/mysql/my.cnf, /etc/mysql/conf.d/*.cnf |
Linux (user-specific) | ~/.my.cnf |
macOS Homebrew | /usr/local/etc/my.cnf, /opt/homebrew/etc/my.cnf, ~/.my.cnf |
Windows | C:\ProgramData\MySQL\MySQL Server 8.0\my.ini, C:\Windows\my.ini |
# Find which config files MySQL is actually reading on your system mysql --help | grep -A1 'Default options' # Output: /etc/my.cnf /etc/mysql/my.cnf ~/.my.cnf # Show which option files were used for a running instance SELECT @@global.datadir; -- data directory also shows install path
Configuration File Structure
# /etc/mysql/my.cnf — annotated production example [client] port = 3306 socket = /var/run/mysqld/mysqld.sock [mysqld] # --- Basic --- user = mysql port = 3306 datadir = /var/lib/mysql socket = /var/run/mysqld/mysqld.sock pid-file = /var/run/mysqld/mysqld.pid # --- Character Set (always set explicitly) --- character_set_server = utf8mb4 collation_server = utf8mb4_unicode_ci # --- InnoDB (most important section) --- innodb_buffer_pool_size = 12G # 75% of 16 GB server innodb_buffer_pool_instances = 8 innodb_log_file_size = 512M innodb_flush_log_at_trx_commit = 1 # ACID safe innodb_io_capacity = 2000 # for SSD # --- Connections --- max_connections = 200 wait_timeout = 28800 interactive_timeout = 28800 thread_cache_size = 16 # --- Binary Log --- log_bin = /var/log/mysql/mysql-bin.log binlog_format = ROW expire_logs_days = 7 sync_binlog = 1 # --- Slow Query Log --- slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1
SET PERSIST (MySQL 8.0+) to change dynamic variables at runtime and have them survive restarts without manual file editing.Critical InnoDB Settings
Variable | Default | Recommended | Description |
|---|---|---|---|
innodb_buffer_pool_size | 128M | 70–80% of RAM on dedicated server | Most impactful setting — caches data and index pages in memory. If your working dataset fits here, most queries avoid disk I/O. |
innodb_buffer_pool_instances | 1 | 1 per GB up to 64 | Splits the buffer pool into multiple independent segments to reduce mutex contention on multi-core servers. |
innodb_log_file_size | 48M | 256M–2G | Larger redo log files mean fewer checkpoint flushes and faster bulk INSERT/UPDATE. Requires clean restart when changed. |
innodb_flush_log_at_trx_commit | 1 | 1 (ACID) or 2 (faster with risk) | 1 = flush redo log on every commit (ACID, safest). 2 = flush every second (up to 1s data loss on crash). 0 = flush every second and skip fsync (dangerous). |
innodb_io_capacity | 200 | 2000+ for SSD | Controls I/O operations InnoDB can perform per second. The 200 default is tuned for spinning disks — raise to 2000–4000 for NVMe/SSD. |
innodb_file_per_table | ON | ON | Each table gets its own .ibd file. Enables TRUNCATE to reclaim space and simplifies table-level operations. |
[mysqld] # InnoDB tuning for a 32 GB dedicated database server with SSD innodb_buffer_pool_size = 24G # 75% of 32 GB innodb_buffer_pool_instances = 8 # one per ~3 GB innodb_log_file_size = 1G innodb_flush_log_at_trx_commit = 1 # ACID compliant innodb_io_capacity = 4000 # NVMe SSD innodb_io_capacity_max = 8000
innodb_log_file_size, stop MySQL cleanly and then remove the old ib_logfile0 and ib_logfile1 files before restarting. MySQL will recreate them at the new size. Never change this on a running server without a clean shutdown.Connection Settings
[mysqld] # Maximum simultaneous connections (each uses ~1 MB RAM) max_connections = 200 # Seconds an idle non-interactive connection waits before MySQL closes it wait_timeout = 28800 # 8 hours (reduce to 300 in production with pooling) # Seconds for interactive sessions (mysql CLI) interactive_timeout = 28800 # Connection backlog queue back_log = 150 # Thread cache — reuse threads instead of creating/destroying per connection thread_cache_size = 32
Sizing max_connections: each connection uses approximately 1 MB of RAM for stack and buffers. Setting it to 1000 uses ~1 GB just for connections. Use a connection pool (ProxySQL, application pool) to keep max_connections under 500.
Binary Log Settings
[mysqld] # Enable binary logging (required for replication and point-in-time recovery) log_bin = /var/log/mysql/mysql-bin.log binlog_format = ROW # safest for replication binlog_row_image = MINIMAL # log only changed columns sync_binlog = 1 # flush to disk on each commit # Retention (MySQL 5.7 uses expire_logs_days; 8.0 uses binlog_expire_logs_seconds) expire_logs_days = 7 # MySQL 5.7 # binlog_expire_logs_seconds = 604800 # MySQL 8.0 (7 days) max_binlog_size = 100M # rotate file when it reaches this size
binlog_format = ROW in production. Statement-based logging (STATEMENT) can produce inconsistent replicas with non-deterministic functions like NOW(), UUID(), and RAND().Viewing Variables at Runtime
-- Show all variables (very long list) SHOW VARIABLES; -- Filter by pattern SHOW VARIABLES LIKE 'innodb_buffer%'; SHOW VARIABLES LIKE 'max_connections'; -- From performance_schema (more detail, filterable) SELECT VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME LIKE 'innodb%' ORDER BY VARIABLE_NAME; -- Check current session variables SHOW SESSION VARIABLES LIKE 'long_query_time';
+----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_buffer_pool_size | 4294967296 | | innodb_buffer_pool_instances | 8 | | innodb_log_file_size | 536870912 | +----------------------------+-------+
Changing Variables at Runtime
-- Change globally (applies to all new connections immediately) SET GLOBAL max_connections = 300; SET GLOBAL slow_query_log = ON; -- Change for current session only (does not affect other connections) SET SESSION long_query_time = 0.5; SET SESSION sql_mode = 'STRICT_TRANS_TABLES'; -- MySQL 8.0+: persist to mysqld-auto.cnf (survives restart, no file editing needed) SET PERSIST max_connections = 300; -- MySQL 8.0+: persist only — does NOT apply to the running instance -- (use for variables that require restart, like innodb_log_file_size) SET PERSIST_ONLY innodb_log_file_size = 1073741824; -- 1G, takes effect after restart
SET PERSIST writes to mysqld-auto.cnf in the data directory. On next startup, MySQL applies both my.cnf and mysqld-auto.cnf, with the latter taking precedence.Dynamic vs Static Variables
Type | Can Change at Runtime? | Examples |
|---|---|---|
Dynamic | Yes — SET GLOBAL or SET SESSION | max_connections, long_query_time, slow_query_log, innodb_buffer_pool_size (8.0+) |
Static | No — requires restart | datadir, socket, port, innodb_log_file_size (pre-8.0) |
Partially dynamic (8.0+) | Some previously static variables are now dynamic | innodb_buffer_pool_size can be resized online in 8.0 |
SET PERSIST and Persistent Variables
-- View what has been persisted via SET PERSIST or SET PERSIST_ONLY SELECT * FROM performance_schema.persisted_variables; -- Remove a persisted variable (reverts to my.cnf value on next restart) RESET PERSIST max_connections; -- Reset ALL persisted variables RESET PERSIST;
mysqld_safe vs systemd
Modern Linux distributions use systemd to manage MySQL:
# systemd (modern Linux — preferred) systemctl start mysql systemctl stop mysql systemctl restart mysql systemctl status mysql systemctl enable mysql # start on boot # mysqld_safe (legacy wrapper script — still used on some systems) mysqld_safe --user=mysql & mysqladmin -u root -p shutdown
Performance Schema Overhead
Performance Schema (P_S) is enabled by default since MySQL 5.7. It adds observability but has a small overhead. In most workloads the overhead is 2–5% and is worth it for the diagnostic value. If you are running a very latency-sensitive workload:
[mysqld] # Disable Performance Schema entirely (not recommended in production) performance_schema = OFF # Or reduce P_S overhead by disabling specific expensive consumers: # Disable in mysql client at runtime: # UPDATE performance_schema.setup_consumers # SET ENABLED = 'NO' WHERE NAME = 'events_statements_history_long';
Key Configuration Quick Reference
Variable | Recommended Value |
|---|---|
innodb_buffer_pool_size | 70–80% of total RAM on a dedicated server |
innodb_buffer_pool_instances | 1 per GB of buffer pool, up to 64 |
innodb_log_file_size | 256M–2G depending on write volume |
innodb_flush_log_at_trx_commit | 1 for ACID; 2 for performance with acceptable risk |
max_connections | No more than 3x the max concurrent app threads; use connection pooling |
slow_query_log | ON — always enable in production |
long_query_time | 1 second is a good starting point; 0.1 for fine-grained tuning |
sync_binlog | 1 for ACID compliance; 0 or 1000 for maximum write speed with data loss risk |
character_set_server | utf8mb4 |
collation_server | utf8mb4_unicode_ci or utf8mb4_0900_ai_ci (MySQL 8.0) |
binlog_format | ROW (safest for replication consistency) |
General Log (Development Debugging)
The general log captures every statement sent to MySQL — useful for debugging application code but far too verbose for production:
-- Enable general log at runtime (do NOT leave this on in production) SET GLOBAL general_log = ON; SET GLOBAL general_log_file = '/var/log/mysql/mysql-general.log'; -- Watch what your application sends to MySQL in real time SHOW VARIABLES LIKE 'general_log%'; -- Disable after debugging SET GLOBAL general_log = OFF;
Error Log Configuration
[mysqld] # Error log — always enable in production log_error = /var/log/mysql/mysql-error.log log_error_verbosity = 2 # 1=errors only, 2=errors+warnings, 3=notes # On systemd systems, log to journald instead # log_error = stderr
Tuning for Specific Workloads
Workload | Key Variables to Tune | Direction |
|---|---|---|
Read-heavy (reporting) | innodb_buffer_pool_size, read_buffer_size | Maximize buffer pool; add read replicas |
Write-heavy (events, logging) | innodb_log_file_size, innodb_flush_log_at_trx_commit, sync_binlog | Larger log files; consider flush=2 for non-critical data |
Mixed OLTP | innodb_buffer_pool_size, max_connections, thread_cache_size | Balanced; use connection pooling; keep transactions short |
Bulk load / ETL | innodb_flush_log_at_trx_commit, foreign_key_checks, unique_checks | Set to 0/OFF during load; re-enable after |
Many small connections | thread_cache_size, back_log, max_connections | Increase thread cache; use connection pooler |
Temporary Table Settings
EXPLAIN showing "Using temporary" means MySQL created an internal temporary table. If this happens frequently for large result sets, increase the in-memory temp table limits:
[mysqld] # Internal temporary table size limits tmp_table_size = 64M # max size of an in-memory temp table max_heap_table_size = 64M # max size of user-created MEMORY tables # If a temp table exceeds tmp_table_size, it spills to disk (much slower) # Monitor with: SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
Verify Configuration is Applied
-- Verify a variable is set to what you expect
SELECT @@global.innodb_buffer_pool_size / 1024 / 1024 / 1024 AS buffer_pool_gb;
SELECT @@global.max_connections;
SELECT @@global.slow_query_log;
-- Check if SET PERSIST has been applied
SELECT * FROM performance_schema.persisted_variables
ORDER BY VARIABLE_NAME;
-- Check running instance vs persisted values
SELECT g.VARIABLE_NAME,
g.VARIABLE_VALUE AS runtime_value,
p.VARIABLE_VALUE AS persisted_value
FROM performance_schema.global_variables g
LEFT JOIN performance_schema.persisted_variables p USING (VARIABLE_NAME)
WHERE p.VARIABLE_VALUE IS NOT NULL
ORDER BY g.VARIABLE_NAME;SQL Mode Settings
SQL mode controls how MySQL handles invalid data and certain SQL syntax. MySQL 8.0 defaults are stricter than 5.7:
-- Check current SQL mode SELECT @@sql_mode; -- MySQL 8.0 default includes STRICT_TRANS_TABLES, which rejects -- invalid data instead of silently truncating or substituting defaults -- Common modes and what they control: -- STRICT_TRANS_TABLES: reject invalid inserts (wrong type, too long, out of range) -- NO_ZERO_DATE: reject '0000-00-00' as a valid date -- NO_ZERO_IN_DATE: reject dates like '2024-00-15' -- ONLY_FULL_GROUP_BY: require GROUP BY to include all non-aggregate SELECT columns -- ERROR_FOR_DIVISION_BY_ZERO: return error instead of NULL for divide by zero -- Set a custom SQL mode (production recommendation) SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,ONLY_FULL_GROUP_BY'; -- In my.cnf: -- [mysqld] -- sql_mode = STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,ONLY_FULL_GROUP_BY
Minimal Starter Configuration for a 4 GB VPS
[mysqld] # Character set character_set_server = utf8mb4 collation_server = utf8mb4_unicode_ci # InnoDB — set to 50-60% of RAM on a shared VPS (OS needs memory too) innodb_buffer_pool_size = 2G innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 1 # Connections max_connections = 100 wait_timeout = 300 # 5 minutes idle timeout thread_cache_size = 8 # Logging slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_error = /var/log/mysql/mysql-error.log
Configuration Best Practices
Start with a validated baseline (mysqltuner.pl provides evidence-based recommendations from your actual status).
Change one variable at a time and benchmark before changing the next.
Use SET PERSIST (MySQL 8.0) to avoid manual my.cnf edits and the risk of config drift between file and runtime.
Monitor SHOW STATUS and SHOW ENGINE INNODB STATUS after changes to verify the expected effect.
Keep a versioned backup of my.cnf — a bad configuration can prevent MySQL from starting.
Run mysqltuner.pl regularly in production and review its recommendations after traffic changes.