Frappe_Claude_Skill_Package frappe-core-cache
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/core/frappe-core-cache" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-core-cache && rm -rf "$T"
manifest:
skills/source/core/frappe-core-cache/SKILL.mdsource content
Frappe Cache & Locking
Quick Reference
| Action | Method | Notes |
|---|---|---|
| Set value | | With optional TTL |
| Get value | | Returns if missing |
| Get or generate | | Calls on cache miss |
| Delete value | | Single key or list of keys |
| Delete by pattern | | Wildcard matching |
| Hash set | | Redis hash field |
| Hash get | | Single hash field |
| Hash get all | | Full hash as dict |
| Hash delete | | Remove hash field |
| Hash exists | | Returns bool |
| Cached document | | Full doc from cache |
| Clear doc cache | | Invalidate cached doc |
| Decorator cache | | Auto-cache function result |
| Request cache | | Per-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
when underlying data changes..clear_cache() - 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
(context manager) to guarantee release.with frappe.lock() - 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 Pattern | Content |
|---|---|
| DocType metadata |
| User permission cache |
| User boot info |
| Notification counts |
| Cached document |
NEVER write to internal cache keys directly. ALWAYS use the documented API methods (
get_cached_doc, clear_document_cache, etc.).
Performance Guidelines
- ALWAYS set TTL on cached values that derive from external data — without TTL, stale data persists until manual invalidation or Redis eviction.
- NEVER cache large objects (>1 MB) — Redis uses pickle serialization, and large values increase serialization overhead and memory usage.
- ALWAYS use
for data needed multiple times within a single request — it avoids Redis round-trips entirely.frappe.local.cache - NEVER use
as a routine invalidation strategy — it clears ALL cache keys for the site, causing a cold-cache performance hit.frappe.clear_cache() - ALWAYS prefix custom cache keys with your app name (e.g.,
) to avoid collisions with Frappe internals.myapp|exchange_rate
Redis Configuration
Default config:
{bench}/config/redis_cache.conf
| Setting | Default | Description |
|---|---|---|
| Port | 13000 | Redis cache port |
| Bind | 127.0.0.1 | Listen address |
| maxmemory-policy | allkeys-lru | Eviction policy |
| maxmemory | 256mb | Max 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
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| Available | Available | Available |
| Not available | Available | Available |
| Not available | Available | Available |
context mgr | Available | Available | Available |
| Available | Available | Available |
with generator | Available | Available | Available |
See Also
- references/examples.md — Cache implementation patterns
- references/anti-patterns.md — Common cache mistakes
- references/api-reference.md — Complete API signatures
— Database queries that benefit from cachingfrappe-core-database
— User permission cachingfrappe-core-permissions