When ERPNext is slow on your own server, the cause is often not ERPNext. It is a default MariaDB install with a 128 MB InnoDB buffer pool trying to serve a database many times that size. Frappe's installer sets the charset; it does not tune the server. A few lines in a my.cnf override file and a restart change how the same box feels. This guide computes those lines from your RAM and database size, shows how to benchmark before and after, and gives a restart order that doesn't leave Frappe half-broken.
A General Ledger that takes 40 seconds on an 8-vCPU cloud server is rarely a hardware problem. It usually traces back to the same few untuned InnoDB settings. I lead ERPNext and Frappe programmes at MithTech, a team of Frappe specialists in Bengaluru. This is the tuning pass our team runs on a self-hosted instance before touching anything else.
What should your my.cnf say for your server?
Type in your VPS RAM, your current database size and your realistic peak of concurrent users. The calculator writes a drop-in .cnf file from the sizing guidance in the MySQL manual on the InnoDB buffer pool and the long-running Frappe forum thread on ERPNext slow performance.
Live-computed my.cnf from your RAM, DB size and expected concurrent users.
Why does a default install make ERPNext slow?
Debian and Ubuntu ship MariaDB with a configuration sized for a laptop, not a production database. innodb_buffer_pool_size defaults to 128 megabytes. That is all the RAM MariaDB will use to cache data pages, index pages, adaptive-hash-index entries and change-buffer state combined.
A year of live transactions takes an ERPNext database into gigabytes, and an import-heavy trading company with GRN, landed cost and stock ledger volume gets there faster. The slow-ERPNext case in the Frappe forum had about 570,000 Journal Entries and 3.3 million GL Entries.
With a 128 MB buffer pool, any query that reads more than a few thousand rows across tabGL Entry, tabStock Ledger Entry, tabPurchase Receipt Item or tabSales Invoice Item evicts its working set on every run. The pool cannot hold enough index pages to answer a General Ledger for a full financial year without going back to disk repeatedly.
Doesn't bench set this up?
It does not, which surprises people.
The MariaDB step in Frappe's installation guide asks you to add this, in full:
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
That is the whole server-level configuration Frappe asks for at install time, and the guide notes that from Frappe v15.21.0 even this step isn't required. Buffer pool, log file, flush behaviour and connection limits are left at whatever your distribution shipped.
bench setup production likewise configures nginx, supervisor, Redis and systemd, but not my.cnf. Frappe doesn't fight the distribution's package manager over a shared config file, so the tuning is on you.
Side-by-side of what the five my.cnf lines actually change.
Fresh apt-get install, zero tuning
128M buffer pool means every ERPNext query past a few MB working set hits disk.
Seven lines of my.cnf, one restart
Working set lives in RAM. Reports return in one screen refresh, not fifteen.
The five (well, seven) my.cnf lines
Here is the block. The tuner above generates one for your inputs; the version below is a starting point for a typical 8 GB VPS running one ERPNext site with a 4–6 GB database, on MariaDB 10.6 or later.
# /etc/mysql/mariadb.conf.d/60-erpnext.cnf
[mysqld]
# --- InnoDB memory ---------------------------------------------
innodb_buffer_pool_size = 5G # 60–70% of RAM
innodb_log_file_size = 1G # ~25% of the pool
innodb_log_buffer_size = 32M
# --- Write / flush behaviour -----------------------------------
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2 # trade durability for throughput
innodb_io_capacity = 400 # 200 = HDD, 400+ = SSD baseline
# --- Connection / packet ---------------------------------------
max_allowed_packet = 64M # required for large report imports
max_connections = 200
thread_cache_size = 32
What the five that matter do:
innodb_buffer_pool_sizeis the most important variable in the file. It sets how much RAM InnoDB uses to cache data pages. The MySQL manual notes that dedicated database servers often give it up to 80% of physical memory. On an ERPNext VPS, MariaDB shares the box with Frappe workers, Redis, nginx and supervisor, so 60% is a sensible target.innodb_log_file_sizesets how much dirty-page work the redo log can absorb before a checkpoint stalls writes. Too small and you get flush storms during write bursts (imports, month-end postings). About 25% of the buffer pool is a common rule of thumb; above 2G, crash recovery gets long.max_allowed_packetat 64M is the value the Frappe forum keeps arriving at (discuss.frappe.io/t/50411). Below it, large report exports, bulk imports and some background jobs fail mid-query with an unhelpful error.innodb_io_capacitytells InnoDB how many IOPS your storage can sustain, which sets the background flush rate. The default 200 suits a spinning disk. On an SSD (NVMe or a cloud gp3 volume) use 400 or higher; the calculator scales it with concurrent users.innodb_buffer_pool_instancesused to split the pool into shards on older MariaDB. From 10.5 the pool is a single instance, and in 10.6 the variable is gone, which is why it is not in the block above. Only on MariaDB 10.4 or older, add it at about one per GB of pool, up to 8.
The remaining two, innodb_flush_method = O_DIRECT and innodb_flush_log_at_trx_commit = 2, trade a slice of durability for throughput. = 2 writes the log to the OS on every commit but fsyncs it only once per second, so a hard crash can lose up to one second of committed transactions. Most self-hosted companies with daily backups accept that; where sub-second durability is a contractual requirement, stay on = 1.
Illustrative query-latency curve — before vs after tuning — as DB size grows.
Where does the config file go?
Do not edit the main my.cnf or mariadb.cnf. Package upgrades overwrite them and your tuning disappears without a warning. Drop an override file into the conf.d directory instead. MariaDB reads every .cnf there in alphabetical order, so 60-erpnext.cnf wins against the shipped defaults.
Debian / Ubuntu (bare metal or VPS)
Path: /etc/mysql/mariadb.conf.d/60-erpnext.cnf. Reload with sudo systemctl restart mariadb. Verify with mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';".
Docker (frappe_docker)
Mount your override as a volume on the mariadb service: ./mariadb.cnf:/etc/mysql/conf.d/60-erpnext.cnf:ro in your compose file.
Bench dev machine (macOS via Homebrew)
Path: $(brew --prefix)/etc/my.cnf.d/60-erpnext.cnf. Reload with brew services restart mariadb. Not needed on a dev box with plenty of RAM, but useful for reproducing production-size reports locally.
RHEL / Rocky / Alma
Path: /etc/my.cnf.d/60-erpnext.cnf. Reload with sudo systemctl restart mariadb. Same content, different directory.
The 60- prefix matters. Package configs usually use 50-server.cnf; a higher number makes sure your file is read last and wins on a conflict.
How do you benchmark before you touch anything?
Most people skip this step. Don't: do not restart a production MariaDB for a speed-up you cannot measure. Without a baseline you can't tell whether the tuning helped, hurt or did nothing, and you can't answer the finance head when the same report is slow next month.
Pick one of the three recipes below (bench, sysbench or a slow-log digest) and run it twice: once now, once after the restart. Report only differences you measured on your own box.
Three copy-paste ways to establish a real before/after baseline: bench execute, sysbench oltp_read_write, pt-query-digest.
How do you restart MariaDB without breaking Frappe?
A cold MariaDB restart with Frappe workers still running fills the error log with MySQL server has gone away for a few seconds until each worker reconnects. It looks bad but does no harm. Still, the clean sequence is:
1. Pause the scheduler so no new background jobs kick off mid-restart
2. Full backup — DB + private files + public files
3. Drop the override file in
4. Sanity-check the file parses BEFORE restarting
5. Restart MariaDB, then Frappe
6. Verify the new values took effect
7. Bring the site back up
Two failure modes:
- The
innodb_log_file_sizechange doesn't apply. Very old MariaDB releases needed a clean shutdown and the oldib_logfile*files deleted before a new size took effect; current releases resize the log on the next start. If you are on an old distribution release, upgrade MariaDB first or leave the log-file change out of the first pass. - The service won't start after the edit. Usually a typo (an extra
M, a missing quote) or a variable your version doesn't know, such asinnodb_buffer_pool_instanceson 10.6.sudo journalctl -u mariadb --no-pager | tail -50shows the exact error. Roll back by deleting60-erpnext.cnfand restarting; restore from the snapshot only if that fails.
When isn't tuning enough?
Tuning fixes "the database had enough RAM for its working set and you weren't letting it use it". It does not fix a database that is too big for one machine, or a query pattern that no buffer pool will save. The signals:
- Buffer-pool hit ratio near 100% and the CPU still saturated. Check with
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'. If almost every read request is served from the pool and reports are still slow, you are CPU-bound on query execution, not I/O-bound on fetching pages. More RAM won't help. - A handful of reports dominate the slow log.
pt-query-digest(the third recipe above) surfaces them. Fix them with indexes or report code, not InnoDB settings. - Writes are the bottleneck. InnoDB tuning helps reads far more than writes. If bulk imports or month-end postings are what's slow, look at batching, a read replica for reporting, or partitioning.
The order we work in: tune first, index second, replica third, partition last. Going straight to a replica because "the database is slow" is a common, expensive mistake when tuning alone would have been enough.
At a glance
| Starting value | ||
|---|---|---|
innodb_buffer_pool_size | ~60% of RAM on a box shared with Frappe | Keeps the working set in memory |
innodb_log_file_size | ~25% of the pool, up to about 2G | Absorbs write bursts without flush storms |
max_allowed_packet | 64M | Large exports and imports stop failing mid-query |
innodb_io_capacity | 400+ on SSD (200 is for spinning disks) | Matches the flush rate to the storage |
innodb_flush_log_at_trx_commit | 2 (up to 1 s of commits at risk on a crash) | Write throughput; stay on 1 if you can't accept that |
innodb_buffer_pool_instances | Leave out on MariaDB 10.6+ | Removed in 10.6 |
Common questions from the IT team
+How much of my server's RAM should the InnoDB buffer pool get?
The MySQL manual notes that dedicated database servers often give the buffer pool up to 80% of physical memory. On an ERPNext VPS, MariaDB shares the machine with Frappe workers, Redis, nginx and supervisor, so 60% is a sensible starting point. The calculator on this page caps it at 70% and checks the headroom against your database size.
+My ERPNext database is bigger than my server's RAM. Is tuning pointless?
No. You can't hold the whole database in memory, so the goal shifts from "keep everything hot" to "keep the working set hot". For most ERPNext workloads the working set is a small part of the whole: a few DocTypes and the last few months of GL entries. A buffer pool sized to most of the RAM usually still holds it. What tuning can't fix is a cold-cache query over three years of data; that is a report-design problem.
+Is it safe to set innodb_flush_log_at_trx_commit = 2 on a production ERPNext?
For most self-hosted ERPNext installs on a modern VPS with daily backups, yes. The trade is that a hard crash (kernel panic, host loss) can lose up to one second of committed transactions, in exchange for higher write throughput. Measure the gain with the benchmark recipes. Where sub-second durability is a contractual requirement, stay on = 1.
+Do I need to change anything in Frappe's site_config.json to match this?
No. Frappe doesn't depend on MariaDB's server-side tuning: each worker opens its own connections through PyMySQL, and the changes here happen inside mariadbd. The one Frappe-side setting worth reviewing alongside this is background_workers in common_site_config.json, which decides how many background workers, and so how many database connections, run at once.
+Why 64M for max_allowed_packet?
64M is the value the Frappe forum thread on ERPNext slow performance arrives at, and it covers the largest single-query payloads ERPNext sends: large report exports, bulk item imports and attachment operations. At the 16M default, some background jobs fail with MySQL server has gone away mid-payload. Higher than 64M works but is rarely needed.
+I restarted MariaDB and ERPNext is still slow. What did I miss?
Three usual causes, most common first. Your override file didn't load: check with SHOW VARIABLES LIKE 'innodb_buffer_pool_size' that it matches the value you set. The buffer pool is still cold: for the first minutes after a restart every query pays to warm it, so wait for ten minutes of real traffic and benchmark again. Or the database was never the bottleneck: watch htop while a slow report runs, and if the Frappe worker is at 100% CPU while MariaDB is idle, the fix is in the report code, not my.cnf.
+Does this apply to ERPNext v14 and v15, or only v16?
The tuning is the same from Frappe v13 onwards, because it sits below the ORM. The values in this post were checked against a Frappe v16.28.0 checkout; the Frappe forum thread they come from dates back to v13/v14, and the guidance hasn't changed. What does change is the MariaDB version: on 10.6 and later, leave out innodb_buffer_pool_instances.
+Is switching from MariaDB to PostgreSQL a better answer?
For most ERPNext users, no. Frappe supports PostgreSQL, but most of the ecosystem's testing, index work and community fixes are on MariaDB. Switch engines only for a specific reason (in-house PostgreSQL skills, a replication setup, an extension you need), not because a benchmark post says so.
What is the bottom line?
Self-hosted ERPNext often gets called slow when the application is fine and the database is running with laptop settings, because the installer never touched them.
Compute your values with the calculator, benchmark against your own workload, restart in a maintenance window with a backup in hand, and benchmark again. If the numbers move, the tuning was the problem; if they don't, the slow log tells you where to look next.
Comments & ratings
No comments yet. Start a new discussion.
Manoj · Published on 24 July 2026
Worked with us? Leave a Google review