Zero-dependency, in-process TTL cache for Python. Optional FastAPI decorators, stampede-safe, LRU-bounded.
Project description
inhouse
Zero-dependency, in-process TTL cache for Python. One decorator, stampede-safe, LRU-bounded. For when Redis is a meeting you don't want to have, or when you need to avoid yet another deployment. Designed to be simple and effective without bloat or complexity for developers.
Designed for easy use with FastAPI applications. Although FastAPI integration is absolutely optional.
Official Documentation/Release Notice:
Complete documentation by version is now managed by Rancero, and is available at https://docs.rancero.com/docs/category/inhouse-cache.
Install
The package is published on PyPI as inhouse-cache. Imports use inhouse (e.g. from inhouse import MemoryStore).
Core:
pip install inhouse-cache
With FastAPI helpers (fastapi_cache, lifespan sweeper):
pip install inhouse-cache[fastapi]
Quick start Usage
Core (any Python project)
from inhouse import MemoryStore, inhouse_cache
store = MemoryStore(max_size=1024, default_ttl=60)
@inhouse_cache(store=store)
async def load_user(user_id: int) -> dict[str, int]:
return {"user_id": user_id}
Works with both async def and def callables.
FastAPI Use Case
import asyncio
from fastapi import FastAPI
from inhouse import MemoryStore
from inhouse.fastapi import create_lifespan, fastapi_cache
store = MemoryStore(max_size=1024, default_ttl=60)
app = FastAPI(lifespan=create_lifespan(store))
@app.get("/items/{item_id}")
@fastapi_cache(store=store)
async def get_item(item_id: int) -> dict[str, int]:
await asyncio.sleep(0.1) # expensive work
return {"item_id": item_id}
Requires pip install inhouse-cache[fastapi].
Features
Core (zero dependencies)
- TTL cache with lazy expiry on read
- LRU eviction when
max_sizeis exceeded - Per-key singleflight stampede guard - concurrent misses on the same key coalesce to one computation. Backend errors propagate to all waiters; client disconnect on the leader no longer aborts in-flight cache population for followers
- Deterministic cache keys - canonical JSON serialization with type-qualified fallbacks for custom objects. Keyword argument order and Request subclasses don't cause spurious cache misses
- Thread-safe store for sync and async callables
- Fixed, store-default, or callable TTL on each cache write
- Opt-in
copy_on_readonMemoryStore— deep-copy cached values on read to prevent caller mutation from corrupting the cache
Optional FastAPI extra (pip install inhouse-cache[fastapi])
@fastapi_cachewith Request/Response-aware cache keys- Background expiry sweeper via FastAPI lifespan helpers
- Clean lifespan shutdown - background sweeper cancels without noisy tracebacks
Configuration reference
MemoryStore
from inhouse import MemoryStore
store = MemoryStore(max_size=1024, default_ttl=60)
| Parameter / attribute | Type | Default | Description |
|---|---|---|---|
max_size |
int |
1024 |
Maximum number of entries before LRU eviction |
default_ttl |
float | None |
None |
Default TTL in seconds for store.set() and decorators that omit ttl_seconds |
copy_on_read |
bool |
False |
When True, get() returns copy.deepcopy() of cached values so callers cannot mutate the store |
default_ttl (property) |
float | None |
— | Mutable at runtime; affects future writes only |
size |
int (read-only) |
— | Current number of cached entries |
Store methods:
| Method | Description |
|---|---|
get(key, *, default=MISS) |
Return a cached value, or default on miss/expiry. Deep-copies when copy_on_read=True |
set(key, value, ttl_seconds=None) |
Write a value; uses default_ttl when ttl_seconds is omitted |
delete(key) |
Remove one entry |
clear() |
Remove all entries |
purge_expired() |
Proactively delete expired entries |
keys() |
List current cache keys |
@inhouse_cache / cache()
Core decorator. Works with both async def and def callables.
from inhouse import MemoryStore, inhouse_cache, make_cache_key
store = MemoryStore(default_ttl=60)
@inhouse_cache(
ttl_seconds=60, # optional — see Dynamic TTL below
store=store, # optional — defaults to a module-level store
key_builder=make_cache_key, # optional — custom cache key strategy
exclude_types=(object,), # optional — types omitted from key material
)
async def load_user(user_id: int) -> dict[str, int]:
return {"user_id": user_id}
| Parameter | Type | Default | Description |
|---|---|---|---|
ttl_seconds |
float | Callable[[], float] | None |
None |
TTL in seconds for each cache write. See Dynamic TTL. |
store |
MemoryStore | None |
module default | Cache instance to read/write |
key_builder |
Callable[..., str] |
make_cache_key |
Builds the cache key from function identity + arguments. Non-JSON-serializable arguments fall back to module.qualname:str(value) |
exclude_types |
tuple[type, ...] |
() |
Argument types excluded from key material (e.g. request objects) |
inhouse_cache is an alias for cache.
Global default store helpers:
from inhouse import configure_default_store, get_default_store
store = MemoryStore(default_ttl=120)
configure_default_store(store)
@inhouse_cache() # uses the configured default store + its default_ttl
async def load_config() -> dict[str, str]:
...
@fastapi_cache (optional — requires inhouse-cache[fastapi])
FastAPI-friendly wrapper around inhouse_cache. Automatically excludes Starlette Request and Response objects from cache keys.
from inhouse.fastapi import create_lifespan, fastapi_cache
store = MemoryStore(max_size=512, default_ttl=60)
app = FastAPI(lifespan=create_lifespan(store, sweep_interval=30.0))
@app.get("/items/{item_id}")
@fastapi_cache(store=store)
async def get_item(item_id: int) -> dict[str, int]:
...
| Parameter | Type | Default | Description |
|---|---|---|---|
ttl_seconds |
float | Callable[[], float] | None |
None |
Same semantics as @inhouse_cache |
store |
MemoryStore | None |
module default | Cache instance to read/write |
key_builder |
Callable[..., str] | None |
make_fastapi_cache_key |
Custom key builder. Defaults exclude Starlette Request/Response; override delegates that responsibility to you |
Custom key_builder functions replace the FastAPI-aware default. To keep Request/Response exclusion, delegate to make_fastapi_cache_key or pass your own exclude_types.
Lifespan / background cleanup (optional — requires inhouse-cache[fastapi])
from inhouse.fastapi import create_lifespan, inhouse_lifespan
# Option A: pass directly to FastAPI
app = FastAPI(lifespan=create_lifespan(store, sweep_interval=30.0))
# Option B: use inside your own lifespan
async with inhouse_lifespan(store, sweep_interval=30.0):
...
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
MemoryStore |
required | Store to sweep for expired entries |
sweep_interval |
float |
30.0 |
Seconds between background purge runs |
Dynamic TTL
TTL is resolved when a value is written to the cache (on a miss), not on every read. Changing TTL settings does not retroactively extend entries already stored.
Three ways to configure expiration:
1. Fixed TTL (per route)
@inhouse_cache(60, store=store)
async def load_user(user_id: int) -> dict[str, int]:
...
Always expires 60 seconds after the value is cached.
2. Store default (mutable at runtime)
store = MemoryStore(default_ttl=60)
@inhouse_cache(store=store)
async def load_config() -> dict[str, str]:
...
# Later - affects future cache writes only
store.default_ttl = 300
Omitting ttl_seconds on the decorator uses store.default_ttl. If both are missing, inhouse raises ValueError.
store.default_ttl is safe to change at runtime from other threads; new writes pick up the updated value atomically.
3. Callable TTL (evaluated on each write)
settings = {"cache_ttl": 60}
@inhouse_cache(lambda: settings["cache_ttl"], store=store)
async def load_dashboard() -> dict[str, str]:
...
settings["cache_ttl"] = 300 # next cache miss uses 300 seconds
Useful for feature flags, config files, or environment-driven TTL without redeploying.
Priority order
When a cache miss is written, TTL is resolved as:
- Callable
ttl_seconds()result, if a callable was passed - Fixed
ttl_secondsfloat, if provided store.default_ttl, if set- Otherwise →
ValueError
When to use inhouse
| Scenario | inhouse | Redis | fastapi-cache2 |
|---|---|---|---|
| Single-node FastAPI prototype | Great | Overkill | Great |
| Zero external infrastructure | Yes | No | Depends on backend |
| Distributed multi-instance cache | No | Yes | Yes (with Redis) |
| Decorator-first developer UX | Yes | No | Yes |
Important limitations
inhouse is per-process memory. If you run uvicorn main:app --workers 4, each worker maintains its own independent cache. That keeps the design simple and avoids shared infrastructure. It is not a distributed cache.
Architecture
flowchart TB
subgraph fastapi_layer [Optional FastAPI Integration - inhouse.fastapi]
FDecorator["fastapi/decorator.py: @fastapi_cache"]
FLifespan["fastapi/lifespan.py: create_lifespan"]
FKeys["fastapi/keys.py: make_fastapi_cache_key"]
end
subgraph core [Zero-Dependency Core]
KeyBuilder["inhouse/keys.py: make_cache_key()"]
Store["inhouse/store.py: MemoryStore"]
Singleflight["inhouse/singleflight.py"]
Sweeper["inhouse/sweeper.py: ExpirySweeper"]
end
FDecorator --> FKeys
FKeys --> KeyBuilder
FDecorator --> Store
FDecorator --> Singleflight
FLifespan --> Sweeper
Sweeper --> Store
Store -->|"OrderedDict + TTL entries"| Memory[(In-Process Memory)]
Core API
The core package has no runtime dependencies. Import from inhouse directly:
from inhouse import MemoryStore, configure_default_store, inhouse_cache, make_cache_key
See Configuration reference for full decorator and store options.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file inhouse_cache-0.1.2.tar.gz.
File metadata
- Download URL: inhouse_cache-0.1.2.tar.gz
- Upload date:
- Size: 11.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1dcb04c7e408fd3c4ba33fca1497185e7557c6d11d415bb88fff280088f51ada
|
|
| MD5 |
6dececc6f01abddff527c034c9be0626
|
|
| BLAKE2b-256 |
761c21f7e8716122705f8bc46c36c92d717c532771797a45695511e258b41771
|
Provenance
The following attestation bundles were made for inhouse_cache-0.1.2.tar.gz:
Publisher:
publish.yml on kineticquant/inhouse
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inhouse_cache-0.1.2.tar.gz -
Subject digest:
1dcb04c7e408fd3c4ba33fca1497185e7557c6d11d415bb88fff280088f51ada - Sigstore transparency entry: 1939421451
- Sigstore integration time:
-
Permalink:
kineticquant/inhouse@53c65892d13a6ce3d01c2ccec8ab676fc8ce5d9f -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/kineticquant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@53c65892d13a6ce3d01c2ccec8ab676fc8ce5d9f -
Trigger Event:
release
-
Statement type:
File details
Details for the file inhouse_cache-0.1.2-py3-none-any.whl.
File metadata
- Download URL: inhouse_cache-0.1.2-py3-none-any.whl
- Upload date:
- Size: 13.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7a0c0256dba767ee712679672b3a006438154759a9093427624492600aafcad
|
|
| MD5 |
d6501db57365d57a5cfdaca871116118
|
|
| BLAKE2b-256 |
03f7f93b51afbcb83051e6a9007700a4673c51b5bd44e2994adceea01562225c
|
Provenance
The following attestation bundles were made for inhouse_cache-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on kineticquant/inhouse
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inhouse_cache-0.1.2-py3-none-any.whl -
Subject digest:
d7a0c0256dba767ee712679672b3a006438154759a9093427624492600aafcad - Sigstore transparency entry: 1939421555
- Sigstore integration time:
-
Permalink:
kineticquant/inhouse@53c65892d13a6ce3d01c2ccec8ab676fc8ce5d9f -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/kineticquant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@53c65892d13a6ce3d01c2ccec8ab676fc8ce5d9f -
Trigger Event:
release
-
Statement type: