How Music Assistant Caches API Responses: A Deep Dive into the CacheController and @use_cache Decorator
Music Assistant uses a SQLite-backed CacheController accessed via mass.cache and driven by the @use_cache decorator to store JSON-serializable API responses with TTL support, stale-while-revalidate behavior, and automatic cleanup.
The music-assistant/server repository implements a sophisticated caching layer to minimize redundant network traffic and improve response times when interacting with external music APIs. At the heart of this system lies the CacheController, which persists JSON-serializable data in a lightweight SQLite database, and the @use_cache decorator that automates cache key generation and retrieval logic. Understanding how Music Assistant handles API response caching is essential for developers extending providers or troubleshooting data synchronization issues.
Core Components of the Music Assistant Caching System
CacheController (SQLite Backend)
The CacheController class in music_assistant/controllers/cache/controller.py serves as the central cache manager, implementing the public API surface including get, set, delete, and clear methods (lines 46-48). It manages a SQLite database file that stores serialized entries with expiration timestamps, checksums, and persistence flags. The controller automatically registers a daily cleanup task at 04:00 UTC to purge expired entries and monitors the database size, logging warnings when the file exceeds the configurable limit of approximately 100 MiB (lines 85-90).
Database Schema
The cache database schema defined in music_assistant/controllers/cache/controller.py (lines 71-84) utilizes two tables: settings and cache. Each cache entry records the provider domain, category identifier, key string, expiration timestamp, checksum hash, JSON payload, and boolean flags indicating whether the entry should persist across manual clear operations and whether expired entries may be served while refreshing in the background.
The @use_cache Decorator
Located in music_assistant/controllers/cache/helpers.py, the @use_cache decorator wraps provider methods to transparently cache their return values (lines 44-57). The decorator constructs deterministic cache keys from the function name and arguments, performs fresh lookups, handles stale-while-revalidate logic, and manages background refresh tasks. When a cache miss occurs, it executes the wrapped function and stores the result via CacheController.set without blocking the caller.
Background Refresh and Cleanup Tasks
When allow_expired_cache=True and a stale entry is encountered, the decorator returns the cached data immediately while scheduling a background refresh task via mass.create_task (lines 33-48). This ensures callers never wait for network I/O while guaranteeing data freshness. The automatic cleanup task removes rows whose expires timestamp is older than the current time, unless protected by the allow_expired_cache flag.
How API Response Caching Works in Practice
When a provider method decorated with @use_cache is invoked, the system executes the following flow:
-
Key Generation: The decorator builds a deterministic key combining the function name and serialized arguments (e.g.,
"get_album_tracks.12345"foralbum_id="12345"). -
Fresh Lookup: The controller queries the SQLite database for a non-expired entry matching the key. If found, it deserializes the JSON payload and reconstructs model objects using the function's return-type hint or a supplied
base_class. -
Stale-While-Revalidate: If the fresh lookup fails and
allow_expired_cache=True, the controller attempts a second lookup accepting expired entries. Upon finding stale data, it returns immediately and schedules a background task to re-execute the original function and update the cache. -
Cache Miss Handling: When no entry exists, the wrapped function executes synchronously, the result is serialized to JSON, and
CacheController.setwrites it to the database in a background task to avoid blocking. -
Automatic Maintenance: Daily cleanup at 04:00 UTC removes expired entries that are not protected by the
allow_expired_cacheflag, preventing unbounded database growth.
Configuration Options for Cache Behavior
The @use_cache decorator and CacheController support several configuration parameters that control caching behavior:
- expiration: TTL in seconds for fresh entries (e.g.,
3600for one hour,86400for one day). - category: Integer identifier for logical grouping (providers typically use the default
0). - persistent: Boolean indicating whether the entry survives manual
clearoperations (useful for immutable data like artist biographies). - cache_checksum: Optional string for cache invalidation when remote data changes (e.g., an ETag hash).
- allow_expired_cache: Enables stale-while-revalidate behavior and protects entries from automatic cleanup.
- allow_bypass: Respects the global
BYPASS_CACHEcontext variable whenTrue(defaults to the opposite ofpersistent).
Real-World Implementation Example
The Spotify provider demonstrates practical usage in music_assistant/providers/spotify/provider.py (lines 230-236):
from music_assistant.controllers.cache.helpers import use_cache
class SpotifyProvider:
@use_cache(3600 * 24, allow_expired_cache=True)
async def get_album_tracks(self, album_id: str) -> list[Track]:
# Performs remote request to Spotify API
response = await self._get(f"albums/{album_id}/tracks")
return [Track.from_dict(item) for item in response["items"]]
For debugging or forced refresh scenarios, you can temporarily disable caching using the BYPASS_CACHE context variable defined in music_assistant/controllers/cache/constants.py (lines 31-34):
from music_assistant.controllers.cache.constants import BYPASS_CACHE
async def force_refresh(spotify_provider, album_id):
with BYPASS_CACHE.set(True):
# This call bypasses the cache entirely
return await spotify_provider.get_album_tracks(album_id)
Summary
- Music Assistant persists API responses in a SQLite database managed by
CacheController(music_assistant/controllers/cache/controller.py). - The
@use_cachedecorator (music_assistant/controllers/cache/helpers.py) automates cache key generation, TTL enforcement, and stale-while-revalidate logic. - Background refresh tasks ensure callers receive immediate responses while data updates occur asynchronously.
- Daily cleanup at 04:00 UTC removes expired entries, with size monitoring to prevent database bloat.
- The
BYPASS_CACHEcontext variable allows temporary cache disablement for debugging purposes.
Frequently Asked Questions
How does Music Assistant handle stale cache entries?
When allow_expired_cache=True is configured on a decorator, the system returns expired data immediately while scheduling a background task via mass.create_task to refresh the entry. This stale-while-revalidate pattern ensures responsive UI behavior while maintaining data freshness. Expired entries are eventually purged during the daily cleanup unless protected by the allow_expired_cache flag.
Can I disable caching for debugging purposes?
Yes. Music Assistant provides the BYPASS_CACHE context variable in music_assistant/controllers/cache/constants.py. When set to True using with BYPASS_CACHE.set(True):, all cache lookups return misses, forcing fresh API requests. This is useful for verifying endpoint behavior or troubleshooting synchronization issues without permanently clearing stored data.
What storage backend does Music Assistant use for caching?
The system uses a lightweight SQLite database stored on the local filesystem. The CacheController manages this database through aiosqlite with a schema supporting JSON serialization, expiration timestamps, and metadata flags. The implementation includes size monitoring with a default warning threshold of approximately 100 MiB to prevent resource exhaustion on low-end devices.
How does the cache key generation work?
The @use_cache decorator constructs deterministic keys by combining the qualified function name with serialized arguments. For example, calling get_album_tracks("12345") generates a key like "get_album_tracks.12345". This approach ensures that identical API calls map to the same cache entry while distinct parameters receive separate storage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →