Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
openaec-foundation avatar

Frappe Core Cache

  • 1 installs
  • 159 repo stars
  • Updated July 8, 2026
  • openaec-foundation/erpnext_anthropic_claude_development_skill_package

Covers Frappe Redis caching with frappe.cache, TTL strategies, cache invalidation, and distributed locking via frappe.lock to prevent stale data.

About

A reference skill for implementing Redis caching, cache invalidation, and distributed locking in Frappe. A developer uses it to avoid stale-cache bugs, race conditions, and unbounded cache keys.

  • Redis caching via frappe.cache set/get with TTL strategies
  • Distributed locking with frappe.lock and cache invalidation patterns

Frappe Core Cache by the numbers

  • 1 all-time installs (skills.sh)
  • Ranked #3,836 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill frappe-core-cache

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars159
Last updatedJuly 8, 2026
Repositoryopenaec-foundation/erpnext_anthropic_claude_development_skill_package

What it does

Covers Frappe Redis caching with frappe.cache, TTL strategies, cache invalidation, and distributed locking via frappe.lock to prevent stale data.

Files

SKILL.mdMarkdownGitHub ↗

Frappe Cache & Locking

Quick Reference

ActionMethodNotes
Set valuefrappe.cache.set_value(key, val)With optional TTL
Get valuefrappe.cache.get_value(key)Returns None if missing
Get or generatefrappe.cache.get_value(key, generator=fn)Calls fn() on cache miss
Delete valuefrappe.cache.delete_value(key)Single key or list of keys
Delete by patternfrappe.cache.delete_keys(pattern)Wildcard * matching
Hash setfrappe.cache.hset(name, key, val)Redis hash field
Hash getfrappe.cache.hget(name, key)Single hash field
Hash get allfrappe.cache.hgetall(name)Full hash as dict
Hash deletefrappe.cache.hdel(name, key)Remove hash field
Hash existsfrappe.cache.hexists(name, key)Returns bool
Cached documentfrappe.get_cached_doc(dt, dn)Full doc from cache
Clear doc cachefrappe.clear_document_cache(dt, dn)Invalidate cached doc
Decorator cache@redis_cacheAuto-cache function result
Request cachefrappe.local.cachePer-request dict (not Redis)

---

Decision Tree

What caching pattern do you need?
│
├─ Cache a function result automatically?
│  ├─ Pure function (same args → same result) → @redis_cache
│  └─ Need custom key/TTL → manual get_value/set_value
│
├─ Cache a document?
│  ├─ Read-only access → frappe.get_cached_doc()
│  └─ Need to invalidate → frappe.clear_document_cache()
│
├─ Cache structured data (multiple fields)?
│  └─ Redis hash → hset/hget/hgetall
│
├─ Per-request cache (avoid repeated DB calls in one request)?
│  └─ frappe.local.cache dict
│
├─ Prevent concurrent execution?
│  └─ Distributed lock → frappe.lock("resource_name")
│
└─ Invalidate cache?
   ├─ Single key → delete_value(key)
   ├─ Pattern → delete_keys("prefix*")
   └─ All site cache → frappe.clear_cache()

---

String Operations

Set and Get

# Set a value (persists until evicted or deleted)
frappe.cache.set_value("exchange_rate_USD", 1.08)

# Set with TTL (expires after N seconds)
frappe.cache.set_value("exchange_rate_USD", 1.08, expires_in_sec=3600)

# Get value (returns None if missing)
rate = frappe.cache.get_value("exchange_rate_USD")

# Get with generator (calls function on cache miss, stores result)
rate = frappe.cache.get_value(
    "exchange_rate_USD",
    generator=lambda: fetch_exchange_rate("USD"),
)

User-Scoped Values

# Store per-user preference
frappe.cache.set_value("dashboard_layout", "compact", user="user@example.com")

# Retrieve for specific user
layout = frappe.cache.get_value("dashboard_layout", user="user@example.com")

Delete

# Single key
frappe.cache.delete_value("exchange_rate_USD")

# Multiple keys
frappe.cache.delete_value(["exchange_rate_USD", "exchange_rate_EUR"])

# Pattern-based deletion (wildcard)
frappe.cache.delete_keys("exchange_rate*")

---

Hash Operations

Use hashes to group related fields under a single key.

# Set hash fields
frappe.cache.hset("config|notifications", "email_enabled", True)
frappe.cache.hset("config|notifications", "sms_enabled", False)
frappe.cache.hset("config|notifications", "max_retries", 3)

# Get single field
email_on = frappe.cache.hget("config|notifications", "email_enabled")

# Get all fields as dict
config = frappe.cache.hgetall("config|notifications")
# {"email_enabled": True, "sms_enabled": False, "max_retries": 3}

# Delete field
frappe.cache.hdel("config|notifications", "sms_enabled")

# Check existence
exists = frappe.cache.hexists("config|notifications", "email_enabled")

Hash with Generator

# hget with generator — calls function on miss
value = frappe.cache.hget(
    "user|permissions",
    "user@example.com",
    generator=lambda: compute_permissions("user@example.com"),
)

---

@redis_cache Decorator

Automatically cache function return values based on arguments.

from frappe.utils.caching import redis_cache

@redis_cache
def get_item_price(item_code, price_list):
    """Expensive query — cached automatically."""
    return frappe.db.get_value("Item Price",
        {"item_code": item_code, "price_list": price_list},
        "price_list_rate",
    )

# First call — hits database, stores in Redis
price = get_item_price("ITEM-001", "Standard Selling")

# Second call — returns from cache
price = get_item_price("ITEM-001", "Standard Selling")

# Clear all cached results for this function
get_item_price.clear_cache()

With TTL

@redis_cache(ttl=300)  # expires after 5 minutes
def get_exchange_rate(from_currency, to_currency):
    return fetch_rate_from_api(from_currency, to_currency)

Rules for @redis_cache:

  • ALWAYS ensure arguments are hashable (strings, numbers, tuples). NEVER pass dicts or lists as arguments.
  • ALWAYS call .clear_cache() when underlying data changes.
  • NEVER use on functions with side effects — the function will NOT execute on cache hits.

---

frappe.local.cache: Request-Scoped Cache

frappe.local.cache is a plain Python dict that lives for the duration of a single HTTP request. It is NOT stored in Redis.

def get_user_settings():
    """Avoid repeated DB calls within a single request."""
    if "user_settings" not in frappe.local.cache:
        frappe.local.cache["user_settings"] = frappe.get_doc(
            "User Settings", frappe.session.user
        )
    return frappe.local.cache["user_settings"]

Use frappe.local.cache when:

  • The same data is needed multiple times in one request
  • The data does NOT need to persist across requests
  • You want zero Redis overhead

---

Document Caching

# Get cached document (read-only, no permission check)
settings = frappe.get_cached_doc("System Settings")
item = frappe.get_cached_doc("Item", "ITEM-001")

# Invalidate when document changes
frappe.clear_document_cache("Item", "ITEM-001")

# Cached single value
val = frappe.db.get_value("Item", "ITEM-001", "item_name", cache=True)

NEVER modify a document returned by frappe.get_cached_doc() — it returns a shared reference. Modifications corrupt the cache for all subsequent reads.

---

Distributed Locking

Prevent concurrent execution of critical sections using Redis-based locks.

# Context manager (recommended)
with frappe.lock("process_payroll"):
    # Only one worker executes this block at a time
    process_all_salary_slips()
    # Lock auto-released on exit

# Manual lock/unlock
frappe.lock("inventory_sync")
try:
    sync_inventory()
finally:
    frappe.unlock("inventory_sync")  # ALWAYS unlock in finally

Rules:

  • ALWAYS use with frappe.lock() (context manager) to guarantee release.
  • NEVER hold locks for more than a few seconds — long locks cause worker starvation.
  • ALWAYS use descriptive lock names to avoid collisions.

---

Cache Invalidation Patterns

Pattern 1: TTL-Based (Time-to-Live)

frappe.cache.set_value("dashboard_stats", compute_stats(), expires_in_sec=300)

Best for: Data that can be slightly stale (exchange rates, dashboard aggregates).

Pattern 2: Event-Based Invalidation

# In hooks.py
doc_events = {
    "Item Price": {
        "on_update": "my_app.cache.invalidate_price_cache",
        "on_trash": "my_app.cache.invalidate_price_cache",
    }
}

# In my_app/cache.py
def invalidate_price_cache(doc, method):
    frappe.cache.delete_keys("item_price*")
    # Or clear specific function cache:
    # get_item_price.clear_cache()

Best for: Data that MUST be fresh immediately after changes.

Pattern 3: Hybrid (TTL + Event)

@redis_cache(ttl=600)
def get_pricing_rules():
    return frappe.get_all("Pricing Rule", fields=["*"])

# Event hook clears cache immediately on change
def on_pricing_rule_update(doc, method):
    get_pricing_rules.clear_cache()

Best for: Frequently read data with occasional updates.

---

Common Cache Keys (Internal)

Key PatternContent
doctype::meta::{dt}DocType metadata
user_permissions::{user}User permission cache
bootinfo::{user}User boot info
notifications::{user}Notification counts
document_cache::{dt}::{dn}Cached document

NEVER write to internal cache keys directly. ALWAYS use the documented API methods (get_cached_doc, clear_document_cache, etc.).

---

Performance Guidelines

1. ALWAYS set TTL on cached values that derive from external data — without TTL, stale data persists until manual invalidation or Redis eviction. 2. NEVER cache large objects (>1 MB) — Redis uses pickle serialization, and large values increase serialization overhead and memory usage. 3. ALWAYS use `frappe.local.cache` for data needed multiple times within a single request — it avoids Redis round-trips entirely. 4. NEVER use `frappe.clear_cache()` as a routine invalidation strategy — it clears ALL cache keys for the site, causing a cold-cache performance hit. 5. ALWAYS prefix custom cache keys with your app name (e.g., myapp|exchange_rate) to avoid collisions with Frappe internals.

---

Redis Configuration

Default config: {bench}/config/redis_cache.conf

SettingDefaultDescription
Port13000Redis cache port
Bind127.0.0.1Listen address
maxmemory-policyallkeys-lruEviction policy
maxmemory256mbMax memory (adjustable)

---

Key Namespacing

All cache keys are automatically prefixed by Frappe with the site name:

# You write:
frappe.cache.set_value("my_key", "value")

# Redis stores:
# "mysite.localhost|my_key"

frappe.cache.make_key(key, user, shared) handles prefixing. The shared=True parameter removes the site prefix for cross-site keys (rare use case).

---

Version Differences

Featurev14v15v16
frappe.cache.set_valueAvailableAvailableAvailable
@redis_cacheNot availableAvailableAvailable
@redis_cache(ttl=)Not availableAvailableAvailable
frappe.lock context mgrAvailableAvailableAvailable
frappe.local.cacheAvailableAvailableAvailable
hget with generatorAvailableAvailableAvailable

---

See Also

  • references/examples.md — Cache implementation patterns
  • references/anti-patterns.md — Common cache mistakes
  • references/api-reference.md — Complete API signatures
  • frappe-core-database — Database queries that benefit from caching
  • frappe-core-permissions — User permission caching

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.