Framework Git

Framework Git

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Performance Optimization

Most slow Frappe sites are slow for one of three reasons: a query without an
index, work being done in the request that should run in the background, or a
value being computed on every request that could be cached. This page covers each,
plus how to find the slow queries in the first place.

Add a database index

If a list view, report, or filter is slow, the usual fix is an index on the column
being filtered or sorted. Add one with add-database-index. For a single column it
creates the index and a Property Setter so the index survives migrations:

bench --site mysite.localhost add-database-index --doctype "Sales Invoice" --column customer

For a multi-column index, pass --column more than once, in the order you want
them indexed:

bench --site mysite.localhost add-database-index --doctype "Sales Invoice" \
  --column customer --column posting_date

A multi-column index is not backed by a Property Setter, so it is not guaranteed to
survive a migrate. To make it permanent, define it in the DocType controller's
on_doctype_update function. Frappe runs this on every bench migrate, so the
index is recreated whenever it is missing:

# sales_invoice.py
def on_doctype_update():
    frappe.db.add_index("Sales Invoice", ["customer", "posting_date"])

on_doctype_update is a module-level function in the controller file, not a method
on the document class. This is how the framework's own DocTypes add their
composite indexes.

You can also mark a single field as indexed in the DocType editor: tick "Index" on
the field.

Indexes speed up reads but slow down writes a little and use disk, so add them for
columns you actually filter or sort on, not every column.

Move work to the background queue

Anything slow that does not need to finish before the response is sent should run
in a background job. The framework has three queues: short, default, and
long, each with its own timeout. Use frappe.enqueue:

import frappe

def send_report():
    # slow work here

frappe.enqueue(send_report, queue="long", timeout=1500)

To run a method on a specific document in the background:

import frappe
frappe.enqueue_doc("Sales Invoice", invoice_name, "send_reminder", queue="default")

Background workers process these queues. If jobs pile up, add workers with the
background_workers config key and regenerate supervisor config (see
Production Setup). Watch the queue depth with
bench --site x doctor, covered in Monitoring.

Caching

Reading from cache (Redis) is far cheaper than recomputing or hitting the
database. The framework gives you a few caching tools.

For a value you read often and rarely changes, use the cache directly:

import frappe

def get_settings():
    value = frappe.cache.get_value("my_settings")
    if value is None:
        value = compute_settings()
        frappe.cache.set_value("my_settings", value)
    return value

For pure functions, the decorators in frappe.utils.caching cache the return
value for you:

from frappe.utils.caching import redis_cache, request_cache, site_cache

@redis_cache(ttl=3600)
def expensive(arg):
    ...

@request_cache          # cached only for the duration of one request
def lookup(arg):
    ...

request_cache lives for a single request, site_cache keeps an in-process
cache per site, and redis_cache stores the result in Redis with a TTL so it is
shared across workers. Remember to clear or expire a cache when the underlying
data changes.

Find slow queries

To know what to index or cache, you need to see which queries are slow.

On MariaDB, turn on the slow query log to capture queries over a threshold. In the
MariaDB shell (bench --site x mariadb):

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;            -- log queries slower than 1 second
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

For a single slow query, prefix it with EXPLAIN to see whether it uses an index:

EXPLAIN SELECT name FROM `tabSales Invoice` WHERE customer = 'ACME';

A row showing type: ALL and no key means a full table scan, that column wants an
index.

Inside the framework, the Recorder (under Recorder in the Desk, or via
bench --site x start-recording) captures the SQL run during requests so you can
see the slow ones and their call counts. Turn on the
Monitor to track request and job durations over
time.

Last updated 2 hours ago
Was this helpful?
Thanks!