Native Caching
Deliver static-site performance with dynamic capabilities. Caspian includes a file-system based caching engine that bypasses the rendering logic for high-traffic routes.
Global Strategy
Control the baseline behavior of your entire application via environment variables. Perfect for switching between development and production modes.
- Environment-based toggles
- Default TTL (Time-to-Live)
Page-Level Overrides
Define specific caching rules per route in your
index.py. Cache
your landing page for hours, but keep your dashboard real-time.
- Declarative Configuration
- Single Source of Truth
1. Global Configuration
.env
In your project root, configure the default behavior. When enabled,
Caspian creates a
caches/
directory to store rendered HTML.
# Enable caching application-wide (default: false) CACHE_ENABLED="true" # Global Time-To-Live in seconds (default: 600s / 10 mins) CACHE_TTL="3600"
2. Granular Control
src/app/**/index.py
Use the declarative
Cache class to
override global settings. This provides a single, type-safe source of
truth for your route's behavior.
from casp.component_decorator import html from casp.cache_handler import Cache # OPTION A: Declarative Configuration # Cache this page for 2 hours (7200s), enabling it explicitly Cache(ttl=7200, enabled=True) # OPTION B: Force Disable (e.g. for user dashboard) # Cache(enabled=False) def page(): return html(r"""<main>Cached blog</main>""")
caches/cache_manifest.json
Valid file exists. Serve HTML directly.
Time: ~2ms
Execute index.py logic ->
Render -> Save to Disk -> Serve.
Caching Logic Matrix
Caspian determines whether to cache a request based on the interaction
between your
.env global
settings and route-specific write settings. In the current
main.py, the cache-read fast path runs only
when CACHE_ENABLED=true.
| Global (CACHE_ENABLED) | Route (Cache Class) | Result |
|---|---|---|
| "false" | None (default) | No Cache |
| "false" | enabled=True | Write Only; Not Reused |
| "false" | enabled=False | No Cache |
| "true" | None (default) | Cache All |
| "true" | enabled=False | Skip New Writes |
main.py calls
is_request_cacheable(request), which returns
False for anything that is not a
GET and for any request with an
authenticated session. This is a hard gate, not a default: a route's
Cache(enabled=True) cannot override it. The
reason is that CacheHandler keys entries on
the URI alone with no session component, so a page rendered for a
signed-in user would be written to caches/
in plain text and later served verbatim to the next visitor. An unreadable
session also counts as not cacheable, since it cannot be proven generic.
enabled=True
cannot force cache reads while the global flag is off, and
enabled=False does not bypass an already
existing cached entry while the global flag is on. Invalidate old entries
when disabling a previously cached route.
Tags And Revalidation
The page-cache key includes the query string, so query variants of one path never share a file.
Declare tags on a route, then invalidate after a successful write. Prefer tags when
one write affects many pages (a category, a listing, a menu) and paths when it affects a known URL.
revalidate_path takes a concrete path such as /blog/python, not a pattern.
# src/app/blog/[slug]/index.py from casp.cache_handler import Cache cache_settings = Cache(ttl=3600, tags=["blog"])
from casp.cache_handler import revalidate_path, revalidate_tag
from casp.rpc import rpc
@rpc(require_auth=True)
async def publish_post(slug: str):
# Save the post first, then invalidate only after the write succeeded.
revalidate_tag("blog") # every page that declared tags=["blog"]
revalidate_path(f"/blog/{slug}") # every query variant of this one path
return {"success": True}
Both helpers are also CacheHandler methods and return the number of entries removed.
invalidate_by_uri(...) still removes exact URIs, including their query string. With
localization enabled the cache stores one document per language, and revalidate_path clears every language.
Data Cache
The page cache stores whole public documents. To cache data — a slow query,
an external API response, a computed dashboard summary — use casp.cache.
from casp import cache
async def page():
summary = await cache.remember("dashboard:summary", 300, load_summary)
...
await cache.put("products:42", product, ttl=600)
product = await cache.get("products:42") # None (or a default) on a miss
await cache.forget("products:42")
CACHE_STORE=memory
Default. A bounded per-process LRU (CACHE_MEMORY_CAPACITY, 10,000).
CACHE_STORE=file
JSON under caches/data, shared by workers on one machine.
CACHE_STORE=redis
Shared across machines. Needs uv add redis and CACHE_REDIS_URL or REDIS_URL; clear() removes only the CACHE_PREFIX.
get,has,put(ttl=Nonenever expires;0or less removes),forever,remember,forget, andclearare all async.- Values are JSON-encoded; dataclasses, datetimes, and Pydantic models come back as plain JSON data.
- Keys are up to 250 characters. Namespace them and include every tenant, identity, locale, and permission dimension the value varies on.
rememberis not single-flight, and correctness or authorization must never depend on a cache entry. Install another backend withcache.configure(store).