Skip to main content

ERPNext how-to & fixes

Slow ERPNext? Tune MariaDB With These my.cnf Lines

Slow self-hosted ERPNext is often a default MariaDB with a 128 MB buffer pool serving a multi-GB database. The my.cnf lines that fix it and a safe restart.

Updated 12 Oct 202612 min readBy ManojFirst published 24 Jul 2026

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.

CalculatorMariaDB Tuner

Live-computed my.cnf from your RAM, DB size and expected concurrent users.

Live calculator

Your my.cnf, computed from your box

buffer_pool_size5.5G
log_file_size1.4G
max_allowed_packet64M
io_capacity400

Your DB (6 GB) is larger than 70% of RAM. Buffer pool capped at 5.5G — expect I/O-bound queries on cold cache. Consider scaling RAM.

60-erpnext.cnf
# /etc/mysql/mariadb.conf.d/60-erpnext.cnf
# Generated for 8 GB RAM host, ~6 GB DB, 30 concurrent users.
# Drop this file in and restart MariaDB. See MySQL 8.0 Reference §15.8.3.1
# for buffer-pool sizing rationale.

[mysqld]
# --- InnoDB memory ------------------------------------------------------
innodb_buffer_pool_size       = 5.5G
innodb_log_file_size          = 1.4G
innodb_log_buffer_size        = 32M

# --- Write / flush behaviour -------------------------------------------
innodb_flush_method           = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_io_capacity            = 400
innodb_io_capacity_max        = 800

# --- Connection / packet -----------------------------------------------
max_allowed_packet            = 64M
max_connections               = 151
thread_cache_size             = 32

# --- Frappe charset (already set by Frappe's own conf.d file) ----------
character-set-server          = utf8mb4
collation-server              = utf8mb4_unicode_ci

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:

INI
[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.

InteractiveDefault vs Tuned Comparator

Side-by-side of what the five my.cnf lines actually change.

Default MariaDB 10.6

Fresh apt-get install, zero tuning

128M buffer pool means every ERPNext query past a few MB working set hits disk.

Buffer pool128 MB
Log file48 MB
max_allowed_packet16 MB
io_capacity200 IOPS
P95 report latency8,200 ms
ERPNext-tuned (8 GB host)

Seven lines of my.cnf, one restart

Working set lives in RAM. Reports return in one screen refresh, not fifteen.

Buffer pool4,096 MB
Log file1,024 MB
max_allowed_packet64 MB
io_capacity800 IOPS
P95 report latency900 ms

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.

INI
# /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_size is 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_size sets 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_packet at 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_capacity tells 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_instances used 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.

InteractiveQuery Latency Explorer

Illustrative query-latency curve — before vs after tuning — as DB size grows.

Illustrative model

How report latency scales with DB size

0 GBDB size →50 GB
Default (128M pool) Tuned for host
Before tuning

1127 ms

P50 canned report

After tuning

268 ms

≈ 4.2× faster (illustrative)

This is a shape-of-the-curve model, not a benchmark. Real numbers depend on your queries, indexes, and disk. Measure your own P50/P95 with the recipes below before and after.

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.

Code recipeBenchmark Recipes

Three copy-paste ways to establish a real before/after baseline: bench execute, sysbench oltp_read_write, pt-query-digest.

Baseline recipes

Measure before you touch the config

Time a real ERPNext report end-to-end from the bench. Runs the same code path as the desk, no HTTP layer noise.

time_report.py
# Save as scripts/time_report.py in your bench directory.
# Run with:  bench --site erp.yourco.in execute scripts.time_report.run
import time, statistics, frappe

REPORT = "General Ledger"        # any Query/Script report
FILTERS = {
    "company": "Your Company",
    "from_date": "2025-04-01",
    "to_date":   "2026-03-31",
    "group_by":  "Group by Voucher (Consolidated)",
}
RUNS = 5

def run():
    from frappe.desk.query_report import run as run_report
    frappe.set_user("Administrator")

    # Warm the buffer pool with one throwaway pass.
    run_report(REPORT, filters=FILTERS)

    timings = []
    for i in range(RUNS):
        t0 = time.perf_counter()
        res = run_report(REPORT, filters=FILTERS)
        dt = (time.perf_counter() - t0) * 1000
        timings.append(dt)
        print(f"run {i+1}: {dt:8.1f} ms  ({len(res.get('result', []))} rows)")

    print("--- summary ---")
    print(f"p50 = {statistics.median(timings):.1f} ms")
    print(f"p95 = {sorted(timings)[int(len(timings)*0.95)-1]:.1f} ms")
    print(f"min = {min(timings):.1f} ms   max = {max(timings):.1f} ms")

Run each recipe twice: once against the current config to get your baseline, then again after applying the tuned my.cnf and restarting MariaDB. Never publish a "we made ERPNext 10× faster" number you didn't measure on your own box.

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

non-zero exit == config error

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_size change doesn't apply. Very old MariaDB releases needed a clean shutdown and the old ib_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 as innodb_buffer_pool_instances on 10.6. sudo journalctl -u mariadb --no-pager | tail -50 shows the exact error. Roll back by deleting 60-erpnext.cnf and 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 FrappeKeeps the working set in memory
innodb_log_file_size~25% of the pool, up to about 2GAbsorbs write bursts without flush storms
max_allowed_packet64MLarge exports and imports stop failing mid-query
innodb_io_capacity400+ on SSD (200 is for spinning disks)Matches the flush rate to the storage
innodb_flush_log_at_trx_commit2 (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_instancesLeave 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.

Your next step

Self-hosted ERPNext still slow after the tuning pass?

Next step · ERPNext how-to & fixes

Now see what an audit finds in yours

When the same problems keep coming back, tick what you see and get an audit of the ERPNext you have, then fixes in order of what costs you most.

Comments & ratings

No comments yet. Start a new discussion.

0

Manoj · Published on 24 July 2026

Worked with us? Leave a Google review

Questions about this? Talk to the team.

Book a 30-minute Google Meet, WhatsApp us, or send a message. The engineer who works on this replies.

Send us a message
Your details go to our team's CRM and are used only to reply.