# How Music Assistant Caches API Responses: A Deep Dive into the CacheController and @use_cache Decorator

> Learn how Music Assistant caches API responses using CacheController and the use_cache decorator. Explore TTL support, stale-while-revalidate, and cleanup.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: deep-dive
- Published: 2026-06-21

---

**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`](https://github.com/music-assistant/server/blob/main/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`](https://github.com/music-assistant/server/blob/main/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`](https://github.com/music-assistant/server/blob/main/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:

1. **Key Generation**: The decorator builds a deterministic key combining the function name and serialized arguments (e.g., `"get_album_tracks.12345"` for `album_id="12345"`).

2. **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`.

3. **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.

4. **Cache Miss Handling**: When no entry exists, the wrapped function executes synchronously, the result is serialized to JSON, and `CacheController.set` writes it to the database in a background task to avoid blocking.

5. **Automatic Maintenance**: Daily cleanup at 04:00 UTC removes expired entries that are not protected by the `allow_expired_cache` flag, 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., `3600` for one hour, `86400` for one day).
- **category**: Integer identifier for logical grouping (providers typically use the default `0`).
- **persistent**: Boolean indicating whether the entry survives manual `clear` operations (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_CACHE` context variable when `True` (defaults to the opposite of `persistent`).

## Real-World Implementation Example

The Spotify provider demonstrates practical usage in [`music_assistant/providers/spotify/provider.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/spotify/provider.py) (lines 230-236):

```python
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`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/cache/constants.py) (lines 31-34):

```python
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`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/cache/controller.py)).
- The `@use_cache` decorator ([`music_assistant/controllers/cache/helpers.py`](https://github.com/music-assistant/server/blob/main/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_CACHE` context 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`](https://github.com/music-assistant/server/blob/main/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.