| name | frappe-ops-performance |
| description | Use when tuning MariaDB, configuring Redis memory, sizing Gunicorn workers, setting up CDN, or profiling slow queries. Prevents performance bottlenecks from default configurations, memory exhaustion, and unoptimized database queries. Covers MariaDB tuning, Redis configuration, Gunicorn worker sizing, CDN setup, slow query log analysis, Python profiling, request profiling. Keywords: performance, MariaDB, Redis, Gunicorn, CDN, slow query, profiling, tuning, optimization, workers, slow page, loading time, ERPNext slow, why is it slow, page takes long, timeout..
|
| license | MIT |
| compatibility | Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16. |
| metadata | {"author":"OpenAEC-Foundation","version":"2.0"} |
Performance Tuning
Frappe/ERPNext performance depends on four layers: database (MariaDB), cache (Redis), application server (Gunicorn), and background workers (RQ). ALWAYS tune all four layers together — optimizing one while ignoring others creates new bottlenecks.
Quick Reference
bench doctor
bench --site mysite.com show-pending-jobs
bench --site mysite.com clear-cache
bench --site mysite.com clear-website-cache
bench purge-jobs
Performance Decision Tree
What is slow?
|
+-- Page loads are slow?
| +-- Check Gunicorn workers (are they saturated?)
| +-- Check MariaDB slow query log
| +-- Check Redis memory (is cache evicting?)
| +-- Enable CDN for static assets
|
+-- Background jobs are delayed?
| +-- bench doctor (check worker count and pending jobs)
| +-- Increase RQ worker count
| +-- Check for long-running jobs blocking queues
|
+-- Database queries are slow?
| +-- Enable slow query log
| +-- Run EXPLAIN on slow queries
| +-- Add indexes on frequently filtered columns
| +-- Use get_cached_value instead of get_value
|
+-- Server runs out of memory?
| +-- Reduce Gunicorn workers
| +-- Set Redis maxmemory
| +-- Check MariaDB innodb_buffer_pool_size
| +-- Look for memory leaks in custom code
|
+-- High CPU usage?
| +-- Profile Python code (cProfile)
| +-- Check for N+1 query patterns
| +-- Review custom scheduled jobs
MariaDB Tuning
Critical Settings
[mysqld]
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
innodb_log_file_size = 256M
innodb_flush_method = O_DIRECT
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
key_buffer_size = 32M
query_cache_type = 0
query_cache_size = 0
max_connections = 200
wait_timeout = 600
interactive_timeout = 600
tmp_table_size = 64M
max_heap_table_size = 64M
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
Slow Query Analysis
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
mysqldumpslow -t 10 -s c /var/log/mysql/slow.log
EXPLAIN SELECT * FROM `tabSales Invoice` WHERE customer = 'ABC';
Index Optimization
SHOW INDEX FROM `tabSales Invoice`;
ALTER TABLE `tabSales Invoice` ADD INDEX idx_customer_date (customer, posting_date);
Redis Configuration
Memory Management
# /etc/redis/redis.conf (or bench config/redis_cache.conf)
# Set maximum memory — NEVER let Redis use all available RAM
maxmemory 512mb
# Eviction policy — allkeys-lru is best for cache use
maxmemory-policy allkeys-lru
# Disable persistence for cache Redis (performance boost)
save ""
appendonly no
Frappe Redis Architecture
Frappe uses THREE Redis instances:
| Instance | Default Port | Purpose | Memory Guide |
|---|
| redis-cache | 13000 | Document cache, session data | 256MB-1GB |
| redis-queue | 11000 | RQ job queues | 128MB-512MB |
| redis-socketio | 12000 | Real-time events | 64MB-256MB |
ALWAYS set maxmemory on redis-cache. Without it, Redis grows unbounded and can trigger OOM killer.
Frappe Caching API
import frappe
frappe.cache.set_value("my_key", {"data": "value"})
result = frappe.cache.get_value("my_key")
value = frappe.db.get_cached_value("Customer", "CUST-001", "customer_name")
frappe.cache.hset("settings", "key1", "value1")
frappe.cache.hget("settings", "key1")
frappe.cache.delete_value("my_key")
frappe.cache.delete_keys("prefix*")
Gunicorn Workers
Worker Count Formula
workers = (2 * CPU_CORES) + 1
Examples:
2 CPU cores → 5 workers
4 CPU cores → 9 workers
8 CPU cores → 17 workers
Configuration
command=/home/frappe/frappe-bench/env/bin/gunicorn \
-b 127.0.0.1:8000 \
-w 9 \
--timeout 120 \
--graceful-timeout 30 \
--max-requests 5000 \
--max-requests-jitter 500 \
frappe.app:application
Memory Calculation
Each Gunicorn worker consumes 150-300MB RAM. ALWAYS verify total memory fits:
Required RAM = workers * 300MB + MariaDB buffer pool + Redis + OS overhead
Example (4 CPU, 8GB RAM server):
9 workers * 300MB = 2.7GB (Gunicorn)
+ 2GB (MariaDB innodb_buffer_pool_size)
+ 1GB (Redis total)
+ 1.5GB (OS + other)
= 7.2GB — fits in 8GB
NEVER set more workers than your RAM allows. Swapping kills performance.
Background Workers (RQ)
Worker Queues
| Queue | Purpose | Default Workers |
|---|
| short | Quick tasks (< 5 min) | 1 |
| default | Standard tasks | 1 |
| long | Heavy tasks (reports, bulk ops) | 1 |
Tuning Worker Count
[program:frappe-bench-frappe-worker-short-1]
command=bench worker --queue short
...
[program:frappe-bench-frappe-worker-short-2]
command=bench worker --queue short
...
docker compose up -d --scale queue-short=3 --scale queue-long=2
Diagnosing Job Backlogs
bench doctor
bench --site mysite.com show-pending-jobs
bench purge-jobs
CDN Setup for Static Assets
{
"cdn_url": "https://cdn.example.com"
}
Monitoring
bench doctor
bench doctor
Key Log Locations
| Log | Path | Contains |
|---|
| Frappe web log | logs/web.log | HTTP requests, errors |
| Worker log | logs/worker.log | Background job output |
| Scheduler log | logs/scheduler.log | Scheduled job execution |
| Site-level log | sites/{site}/logs/ | Per-site errors (v13+) |
| Slow query log | /var/log/mysql/slow.log | Slow database queries |
Scheduled Job Log (DocType)
Check Setup > Scheduled Job Log in ERPNext UI for:
- Job execution times
- Failed jobs with error details
- Frequency analysis
RQ Dashboard (Optional)
pip install rq-dashboard
rq-dashboard --redis-url redis://localhost:11000
Common Bottleneck Diagnosis
| Symptom | Likely Cause | Solution |
|---|
| Slow page loads, high DB time | Missing indexes, N+1 queries | Add indexes, use get_list with filters |
| Worker queue growing | Too few workers, long jobs | Increase workers, optimize job code |
| High memory, OOM kills | Too many Gunicorn workers, Redis unbounded | Reduce workers, set maxmemory |
| Intermittent timeouts | Gunicorn timeout too low | Increase --timeout (default 120s) |
| Slow after cache clear | Cold cache, no warming | Pre-warm critical caches after deploy |
| Static assets slow | No CDN, no browser caching | Add CDN, set expires headers |
Scaling Patterns
Vertical Scaling (single server):
1. Add RAM → increase innodb_buffer_pool_size + Redis maxmemory
2. Add CPU → increase Gunicorn workers + RQ workers
3. Use SSD → dramatic improvement for database I/O
Horizontal Scaling (multiple servers):
1. Separate DB server (MariaDB on dedicated host)
2. Separate Redis server(s)
3. Multiple app servers behind load balancer
4. Read replicas for reporting queries
5. Kubernetes with frappe_docker for auto-scaling
Version Differences
| Feature | v14 | v15 | v16 |
|---|
| Site-level logs | v13+ | Yes | Yes |
bench doctor | Yes | Yes | Yes |
| Scheduled Job Log | Yes | Yes | Yes |
get_cached_value | Yes | Yes | Yes |
| Background workers (RQ) | Yes | Yes | Yes |
Reference Files
Related Skills
frappe-ops-deployment — Production deployment setup
frappe-ops-backup — Backup and disaster recovery
frappe-ops-bench — Bench CLI reference
frappe-core-database — Database API and query patterns