Mastering Caching in Frappe: Practical Use Cases Beyond the Basics

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.


:arrows_counterclockwise: 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.


:brain: 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.


:jigsaw: 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) 

:soap: 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.


:brain: 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.


:compass: 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.

:speech_balloon: 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.


:globe_with_meridians: 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

5 Likes