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

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. 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 (and similarly in 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 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 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 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 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:


# 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.

# 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`

# 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 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 and reused across scans unless explicitly cleared.
  • The SteamGridDBService and SGDBBaseHandler in backend/adapters/services/steamgriddb.py and 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 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. 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →