# How RomM Implements Artwork Caching for SteamGridDB: A Technical Deep Dive

> Discover RomM's technical deep dive into artwork caching for SteamGridDB. Learn how RomM uses Redis and relational databases to eliminate API calls and optimize storage.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: deep-dive
- Published: 2026-07-05

---

**RomM uses a two-layer caching strategy that stores static SteamGridDB metadata in Redis and persists game cover URLs directly in the relational database, eliminating redundant API calls while minimizing storage overhead.**

RomM is an open-source ROM manager that integrates with SteamGridDB (SGDB) to fetch high-quality artwork for games. Understanding how RomM handles artwork caching for SteamGridDB is essential for administrators optimizing performance and managing API rate limits. Rather than storing actual image files from SGDB, the implementation caches metadata and URLs to create a lightweight, resilient system.

## The Two-Layer Caching Architecture

RomM’s approach separates concerns between **static configuration data** and **dynamic game-specific artwork references**. This architecture resides in two distinct storage layers.

### Redis Cache for Static Metadata

When the application starts, RomM loads static SGDB-related JSON fixtures into a Redis-backed cache via [`backend/utils/cache.py`](https://github.com/rommapp/romm/blob/main/backend/utils/cache.py). The helper function `conditionally_set_cache` stores the file’s JSON payload together with an MD5 hash, ensuring subsequent application starts skip the load if the fixture has not changed.

This provides a fast, in-memory source for SGDB-related constants such as supported dimensions and MIME types. For example, the system caches the SGDB dimensions fixture using a key like `"sgdb:dimensions"`, allowing rapid lookup without filesystem I/O during normal operation.

### Database Persistence for Cover URLs

For each ROM that is scanned, the system caches the selected artwork URL in the **relational database** itself. The `url_cover` field in [`backend/models/rom.py`](https://github.com/rommapp/romm/blob/main/backend/models/rom.py) (and similarly in [`backend/models/collection.py`](https://github.com/rommapp/romm/blob/main/backend/models/collection.py)) stores the direct URL to the cover image provided by SteamGridDB.

This URL survives application restarts and is reused on later scans without contacting the SGDB API again. The cache is only invalidated when a ROM is rescanned with `--force-refresh` or when the cover URL is manually cleared.

## The End-to-End Caching Flow

The implementation follows a specific pipeline from API request to persistent storage:

1. **API Request**: `SteamGridDBService` in [`backend/adapters/services/steamgriddb.py`](https://github.com/rommapp/romm/blob/main/backend/adapters/services/steamgriddb.py) makes an async HTTP GET to `https://steamgriddb.com/api/v2/grids/game/{game_id}`. The request is wrapped with authentication middleware that injects the API key.

2. **Grid Iteration**: `SGDBBaseHandler._get_game_covers` in [`backend/handler/metadata/sgdb_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/metadata/sgdb_handler.py) calls `SteamGridDBService.iter_grids_for_game(...)`. This async generator yields each grid record; the handler collects them into a list.

3. **URL Selection**: Methods like `get_rom_by_id` or `get_details_by_names` in the SGDB handler iterate through the grids and pick the first entry containing a non-empty `url` field. The handler creates an `SGDBRom` dictionary containing `sgdb_id` and `url_cover`.

4. **Database Storage**: `ScanHandler` in [`backend/handler/scan_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/scan_handler.py) processes the ROM and invokes the SGDB handler. If a `url_cover` is present and no manual cover is preserved, it writes the URL into the ROM attributes: `rom_attrs["url_cover"] = sgdb_cover`. This persists the value to the database column `url_cover`.

5. **Cache Hit**: On subsequent scans, the handler first checks `rom_attrs.get("url_cover")`. If the URL already exists, no SGDB request is made, effectively serving the artwork from the database cache.

6. **Optional Local Download**: If the UI requires a local copy, [`backend/handler/filesystem/resources_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/filesystem/resources_handler.py) fetches the cached URL, stores the image under `covers/…`, and serves it. This step is on-demand and separate from the core SGDB caching mechanism.

## Implementation Details and Code Examples

The following examples demonstrate the key caching mechanisms as implemented in the RomM source code:

```python

# Creating the SGDB service and fetching grids

from backend.adapters.services.steamgriddb import SteamGridDBService

service = SteamGridDBService()

# Fetch all grids for a game (internal to the handler)

async for grid in service.iter_grids_for_game(game_id=123):
    print(grid["url"])  # Each grid dict contains 'thumb', 'url', etc.

```

```python

# Scan handler snippet that stores the cached URL

# Located in backend/handler/scan_handler.py

if sgdb_cover and not manual_cover_preserved:
    rom_attrs["url_cover"] = sgdb_cover  # Persists to DB column `url_cover`

```

```python

# Conditional cache loader for static metadata (run at startup)

# Located in backend/utils/cache.py

from backend.utils.cache import conditionally_set_cache
from config.config_manager import METADATA_FIXTURES_DIR

await conditionally_set_cache(
    async_cache,
    key="sgdb:dimensions",
    file_path=METADATA_FIXTURES_DIR / "sgdb_dimensions.json",
)  # Stores JSON + MD5 hash in Redis for fast reuse

```

## Why This Design Works

This caching strategy provides several technical advantages for production deployments:

*   **Rate Limit Protection**: The SteamGridDB API enforces rate limits; by persisting the selected cover URL in the ROM record, RomM avoids repeated look-ups for the same game.
*   **Minimal Storage Overhead**: The system stores only URLs and metadata JSON rather than full image binaries, reducing database bloat.
*   **Fast Reads**: The combination of Redis for static constants and the relational database for dynamic URLs provides sub-millisecond lookup times for cached entries.
*   **Flexible Refresh**: Administrators can force a refresh using `--force-refresh` or by clearing the `url_cover` field, giving fine-grained control over cache invalidation.

## Summary

*   RomM implements artwork caching for SteamGridDB through a dual-layer approach: Redis caches static metadata fixtures, while the relational database stores per-ROM cover URLs.
*   The `conditionally_set_cache` function in [`backend/utils/cache.py`](https://github.com/rommapp/romm/blob/main/backend/utils/cache.py) manages Redis entries with MD5 hash validation to prevent unnecessary reloads.
*   Cover URLs are persisted in the `url_cover` field of [`backend/models/rom.py`](https://github.com/rommapp/romm/blob/main/backend/models/rom.py) and reused across scans unless explicitly cleared.
*   The `SteamGridDBService` and `SGDBBaseHandler` in [`backend/adapters/services/steamgriddb.py`](https://github.com/rommapp/romm/blob/main/backend/adapters/services/steamgriddb.py) and [`backend/handler/metadata/sgdb_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/metadata/sgdb_handler.py) handle API communication and URL selection.
*   Actual image files are fetched on-demand only when the UI requires local copies, keeping the core cache lightweight and API-efficient.

## Frequently Asked Questions

### Does RomM store actual image files from SteamGridDB?

No. RomM caches only the metadata and the URL to the artwork. The actual image files remain hosted on SteamGridDB's servers. If the UI requires a local copy, the [`resources_handler.py`](https://github.com/rommapp/romm/blob/main/resources_handler.py) fetches the URL on-demand and stores it temporarily, but this is separate from the core caching mechanism that persists URLs in the database.

### How can I force a refresh of cached SteamGridDB artwork?

You can force a refresh by running a scan with the `--force-refresh` flag, which bypasses the cached `url_cover` values. Alternatively, you can manually clear the `url_cover` field in the database for specific ROMs, causing the next scan to re-query the SteamGridDB API for new artwork URLs.

### Where is the cached cover URL stored in the database?

The cached cover URL is stored in the `url_cover` column of the ROM table, defined in [`backend/models/rom.py`](https://github.com/rommapp/romm/blob/main/backend/models/rom.py). This field is populated by the `ScanHandler` during the metadata enrichment phase and is checked before making subsequent API calls to SteamGridDB.

### What happens if SteamGridDB is unavailable when scanning?

If SteamGridDB is unavailable and a ROM does not have a cached `url_cover`, the scan will proceed without artwork for that title. If the ROM already has a `url_cover` value in the database, the system will use that cached URL regardless of SteamGridDB's availability, allowing the ROM to display artwork even when the external service is down.