Caching is often the invisible force that makes your Frappe and ERPNext apps feel snappy and responsive — but it’s not just a performance hack. In the Frappe Framework, caching is deeply woven into everything from user preferences to document fetching and function results.
In this article, we’ll go beyond the basics and explore how caching actually works under the hood in Frappe, and when you can (and should) tap into it.
How Frappe Caching Works (The Short Version)
Frappe uses Redis as a fast, in-memory key-value store. When you run bench start, Redis is launched as a service and becomes available to workers through the frappe.cache interface.
Behind the scenes, Frappe smartly wraps Redis to handle things like:
- Site-level key separation (multi-tenancy)
- Automatic serialization (via Python’s pickle)
- Short-term in-request caching via frappe.local.cache
So you don’t need to think about low-level Redis code. Just use the API.
Real Examples of Caching in Action
1. User Filter Settings in List View
Every time you apply filters in the List view (e.g., filtering “Customers” where “Territory = India”), Frappe stores that in Redis temporarily.
Later — often after about an hour — this cache syncs with the __User Settings table in the database.
That’s why:
- Your filters persist across sessions
- But they update a little later in the DB
So technically, it’s:
Frontend → Redis (live settings) → Periodic sync → DB (__User Settings)
Why cache it first?
- Fast load times
- No need to hit DB for every UI interaction
2. Custom Column Visibility in Child Tables
Say you hide or rearrange columns in a child table (like Item rows in Sales Invoice). That state is cached per user too.
This is especially important for large tables — imagine 100+ invoices with 10 columns each. If Frappe had to calculate visible columns every time, it would slow down quickly.
Cached columns are saved:
- As JSON in Redis
- Synced to __User Settings periodically
And yes — your settings apply across devices, thanks to this hybrid caching model.
3. Frequently Accessed Master Data
Let’s say your app needs to fetch the default currency for a company over and over again.
Instead of writing:
frappe.db.get_value("Company", company, "default_currency")
Just do:
frappe.get_cached_value("Company", company, "default_currency")
This will:
- Check Redis first
- Fall back to DB only if needed
- Automatically invalidate the cache if that company record changes
You save a DB query every time. No extra code needed.
Custom Caching: When and How
For expensive logic, caching can be added manually. Let’s say you have a heavy calculation for generating reports:
def compute_profit_margin(item_code):
# Some long logic here
return margin
Add cache like this:
def compute_profit_margin(item_code):
key = f"profit_margin|{item_code}"
if cached := frappe.cache.get_value(key):
return cached
margin = heavy_logic(item_code)
frappe.cache.set_value(key, margin, expires_in_sec=3600)
return margin
Or better, use the built-in decorator:
from frappe.utils.caching import redis_cache
@redis_cache(ttl=3600)
def compute_profit_margin(item_code):
return heavy_logic(item_code)
Invalidation Matters
Stale data is worse than no cache at all. Frappe helps automatically in some cases:
- When you call .save() on a document
- Or use frappe.db.set_value(…)
But manual invalidation is sometimes required:
frappe.cache.delete_value("profit_margin|ITEM-0001")
Or if you’re using @redis_cache:
compute_profit_margin.clear_cache()
Pro tip: always plan how and when cached data should expire.
Advanced: Function-Specific vs Document Caching
Use get_cached_doc(…) if:
- You want a full document
- It rarely changes (like System Settings)
Use @redis_cache if:
- You’re writing pure logic
- The output depends only on input arguments
These aren’t interchangeable — they solve different problems.
Key Takeaways
- Redis is already powering much of your Frappe experience behind the scenes.
- You can cache logic (@redis_cache) or documents (get_cached_doc).
- User preferences like filters and columns are cached first, then synced to DB.
- Always set expiry or clear cache manually to prevent stale data.
- Don’t over-cache — cache what doesn’t change often.
Bonus: When Not to Use Cache
Avoid caching when:
- The data changes frequently (e.g., stock balance)
- Real-time accuracy is critical (e.g., payment status)
- You’re not handling invalidation properly
Remember: cache is an optimization, not a replacement for real-time data when it’s needed.
Next Steps
Want to dig deeper?
- Explore frappe.utils.redis_wrapper for how Redis is wrapped
- Check frappe.cache.make_key to understand key scoping in multi-site setups
- Try monitoring cache usage in production and measuring impact
Contact: Sudhanshu Badole