Skip to content

Cache cms_settings reads instead of querying on every render - #194

Merged
ssavutu merged 1 commit into
mainfrom
perf/cache-cms-settings
Aug 7, 2026
Merged

Cache cms_settings reads instead of querying on every render#194
ssavutu merged 1 commit into
mainfrom
perf/cache-cms-settings

Conversation

@ssavutu

@ssavutu ssavutu commented Aug 6, 2026

Copy link
Copy Markdown
Member

Follow-up to #193. That raised the pool and stopped the outage; this removes the reason the queue formed in the first place.

The problem

cms_settings is read on nearly every public page — site title and footer on every layout, carousel and developing stories on the homepage — and written a few times a month by an editor. Every read was its own round trip. The moment www went public, a handful of near-constant values became the dominant query load, and with a ten-connection pool they queued in front of everything else: /v1/settings/site was taking 25–125 s while the database itself sat at two running threads.

The change

Reads and writes share one pair of helpers in settings_cache.go. The read caches per key with a 30 s TTL; the write upserts and drops that key, so an editor's save shows up on the next request rather than up to a TTL later.

Every writer moved onto the shared helper — including the startup articles_seo_backfilled flag. That one is written once and never read hot, but leaving a single inline INSERT behind is exactly how a cache quietly diverges from its table months later. There are now no direct cms_settings reads or writes outside this file.

Three details that decide whether this is correct

  • Errors are never cached. A failed read must not pin an empty value for the whole TTL and turn a blip into 30 seconds of wrong output.
  • Absence is cached, and separately from emptiness. Callers distinguish "no row" (use the built-in default) from "row present but blank" (a deliberate empty value — breaking-news text relies on this). The entry records both the value and whether the row existed, so caching a miss can't silently change behavior.
  • SETTINGS_CACHE_TTL_SECONDS=0 disables it outright, the escape hatch if a stale value is ever suspected of hiding a bug.

Blue/green runs two processes, but only the active slot serves traffic, so the standby's cache is cold rather than wrong.

Tests

Six new unit tests, including one that passes a nil *sql.DB to a cached read — if the cache ever falls through to a query it panics, rather than appearing to pass because a real connection happened to be available. Also covers TTL parsing (default / override / 0 / invalid), cached absence, invalidation, expiry, and that a disabled cache stores nothing.

Full suite green: go build ./..., go vet ./..., go test ./....

🤖 Generated with Claude Code

cms_settings is read on nearly every public page -- site title and footer on
every layout, carousel and developing stories on the homepage -- and written a
few times a month by an editor. Each read was its own round trip, so a handful
of near-constant values became the dominant query load the moment the site went
public, and with a small pool they queued in front of everything else. Raising
the pool (#193) stopped the outage; this removes the reason the queue formed.

Reads and writes now share one pair of helpers. The read caches per key with a
30s TTL; the write upserts and drops that key, so an editor's save is visible on
the next request rather than up to a TTL later. Every writer moved onto the
shared helper, including the startup backfill flag: leaving a single inline
INSERT behind is exactly how a cache diverges from its table months later.

Three details that decide whether a cache like this is correct:

Errors are never cached. A failed read must not pin an empty value for the whole
TTL and turn a blip into 30 seconds of wrong output.

Absence is cached, and separately from emptiness. Callers distinguish "no row"
(use the built-in default) from "row present but blank" (a deliberate empty
value, e.g. breaking-news text), so the cache records both the value and whether
the row existed.

SETTINGS_CACHE_TTL_SECONDS=0 disables it outright, which is the escape hatch if
a stale value is ever suspected of hiding a bug.

Blue/green runs two processes; only the active slot serves traffic, so the
standby's cache is cold rather than wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ssavutu
ssavutu merged commit 406a022 into main Aug 7, 2026
6 checks passed
@ssavutu
ssavutu deleted the perf/cache-cms-settings branch August 7, 2026 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant