If you are evaluating ERPNext Development Services, the difficult part is rarely adding another DocType or custom field. Production performance is usually harder.
A custom ERPNext report can work well with a small dataset and become painfully slow after months of invoices, stock movements, or accounting records. The first reaction is often to increase server resources. That can hide the symptom without fixing the query.
In this article, we focus on one specific failure mode: unbounded database reads combined with permission-aware queries.
We will reduce the result set at the database layer, understand the difference between get_list() and get_all(), inspect query execution, and move expensive operations into background jobs.
For broader implementation work, you can also explore our ERPNext Development Services.
1. Start With the Query, Not the Server
The first principle behind effective ERP customization is simple: identify the expensive operation before changing infrastructure.
A common custom-report pattern loads every matching record and then filters those records in Python:
# The inefficient part is retrieving every row before applying the business condition.
records = frappe.db.get_all(
"Sales Invoice",
fields=["name", "customer", "posting_date", "grand_total"]
)
records = [
row for row in records
if row["grand_total"] > 10000
]
The problem is not the Python syntax. The problem is where the filtering happens.
The database has already returned the complete result set. Python then processes rows that the report will eventually discard.
Push that condition into the database instead:
# Filtering and pagination happen before records reach the application layer.
records = frappe.db.get_all(
"Sales Invoice",
filters={
"grand_total": [">", 10000]
},
fields=[
"name",
"customer",
"posting_date",
"grand_total"
],
order_by="posting_date desc",
start=0,
page_length=50
)
Frappe's database API supports filters, selected fields, ordering, offsets, and page lengths through its list APIs.
The important design decision is therefore not simply "use fewer records." It is to make the database perform filtering and pagination before the application receives the data.
2. Treat Permissions as Part of the Query
That optimization creates another question: should the report respect the current user's permissions?
This matters because ERPNext Development Services often involve custom reports that expose financial, inventory, HR, or customer information.
Frappe documents an important distinction. frappe.db.get_list() applies user permissions for the current session, while frappe.db.get_all() returns records without applying those permissions.
For a permission-aware report:
# get_list applies the current user's permission rules.
rows = frappe.db.get_list(
"Sales Invoice",
filters={"docstatus": 1},
fields=["name", "customer", "grand_total"],
page_length=50
)
For an intentionally unrestricted internal operation:
# get_all bypasses user permissions, so this choice must be intentional.
rows = frappe.db.get_all(
"Sales Invoice",
filters={"docstatus": 1},
fields=["name", "customer", "grand_total"],
page_length=50
)
These methods should not be treated as interchangeable shortcuts.
The choice affects both the data returned and the execution path. Frappe's current documentation also notes breaking changes to get_list() and get_all() from version 16 onward.
3. Inspect the Execution Plan Before Adding Indexes
Once the query is constrained, the next question is whether the database can execute it efficiently.
This is where ERPNext Development Services move from application customization into database troubleshooting.
Start by identifying active database work:
-- Inspect active database operations before changing the schema.
SHOW PROCESSLIST;
Then inspect the query plan:
-- EXPLAIN shows how the database intends to execute the query.
EXPLAIN
SELECT name, customer, posting_date, grand_total
FROM `tabSales Invoice`
WHERE docstatus = 1
ORDER BY posting_date DESC;
Frappe's performance guidance recommends examining active queries and using EXPLAIN when investigating database performance.
The useful sequence is straightforward: identify the query, inspect its plan, and only then consider schema changes.
Do not automatically add an index because a report is slow. First check the predicate, ordering, returned columns, and actual execution plan.
A query that applies a function to a column inside a filter can also prevent efficient index usage. If a date filter can be expressed as a range, test that form against the actual execution plan.
4. Move Expensive Operations Out of the Request
Even a well-optimized query may not belong inside an interactive HTTP request.
Large exports, bulk synchronization, document generation, and external API processing can take longer than a user should wait for a browser response.
That is where ERPNext Development Services often require an architectural decision rather than another query optimization.
Frappe provides background jobs through frappe.enqueue(). Its documentation supports short, default, and long queues for asynchronous work.
For example:
# Queue the export so the browser request does not wait for completion.
frappe.enqueue(
"my_app.tasks.generate_sales_export",
queue="long",
filters=filters
)
This does not make the underlying operation faster.
Instead, it changes where the waiting happens. The browser can receive an acknowledgement while the worker processes the expensive operation.
That distinction matters. Background processing is a request-latency strategy, not a replacement for database optimization.
Real-World Application
That trade-off between query optimization and asynchronous processing is where ERPNext Development Services become particularly practical.
We implemented a similar pattern in an anonymized ERPNext Development Services environment where reporting workloads shared database resources with transactional operations. The initial approach considered additional infrastructure capacity, but that would not have addressed unnecessary data retrieval.
We instead constrained report queries, selected only required fields, introduced pagination, and moved long-running exports into Frappe background jobs.
The project-specific performance result should be inserted from your actual monitoring data.
Do not publish a number such as "70% faster" unless you can identify how it was measured. Record the ERPNext/Frappe versions, database engine, server resources, dataset size, request volume, and measurement method.
For broader ERP implementation and customization context, you can also explore the Oodles homepage.
Version Context Matters
Version differences should be part of the debugging record.
As of September 30, 2026, the ERPNext repository lists v16.37.0 among its releases. Recent v16 releases also include permission-related changes, making version-specific reproduction important when investigating application behavior.
Capture the installed versions before comparing environments:
# Record exact application versions before comparing environments.
bench version
Do not assume that behavior observed on an older ERPNext installation applies unchanged to the current release.
This is particularly important when troubleshooting customized installations where application code, framework versions, patches, and custom apps can all influence the result.
Key Takeaways
- Database filtering should happen before application-level processing. Avoid retrieving thousands of records only to discard most of them in Python.
-
get_list()andget_all()have different permission behavior. Choose between them deliberately rather than treating them as performance alternatives. - Use
EXPLAINbefore adding indexes. The execution plan tells you whether the database can use the available access path efficiently. - Pagination protects interactive requests. A report should not retrieve every historical transaction when the interface displays only a small page.
- Background jobs are useful for asynchronous workloads. Frappe's
frappe.enqueue()provides a supported mechanism for moving expensive work away from the request path.
If you have solved a similar ERPNext performance problem, share the query pattern, version, and measurement method. For implementation discussions, you can also visit our Contact Us page.
FAQ
What do ERPNext Development Services include?
ERPNext Development Services can include custom DocTypes, workflows, reports, integrations, application customizations, performance optimization, migrations, and business-specific ERP functionality.
When should I consider ERPNext customization?
Consider customization when standard ERPNext functionality does not match a specific business process, integration requirement, reporting workflow, or scalability constraint.
Can custom ERPNext development improve report performance?
Yes. Custom development can address inefficient queries, excessive data retrieval, permission-related behavior, pagination, indexing, and asynchronous processing. The appropriate fix depends on the measured bottleneck.
Top comments (0)