Skip to content

The Settings Row Was Right and the API Was Wrong

Adityo Guni Waluyo

A direct SQL insert updated the row but the settings API kept serving the old value from its boot-time snapshot. The three-layer debug order.

TL;DR

A direct SQL write updated the row, but the settings API kept serving the old value because it answers from an in-memory snapshot built at boot. The footer and JSON-LD disagreeing in one render signaled a freshness problem, not a storage one. Restart the API after direct writes, and verify layer by layer before trusting any single reader.

The insert went in clean. The row held the new value, updated_at had moved to the current timestamp, and the settings API still returned the old one. On the server-rendered page, the footer showed the new value while the JSON-LD block in the head kept the previous one. One request, one render pass, two answers.

This was a row written directly through SQL rather than through any admin surface, so the write path itself was never involved.

The first theory pointed at the wrong layer

The obvious suspects were environmental. The API process could be reading a different database, a different schema, or a different host. All of that checked out. The connection string pointed at the same instance the write landed in, the table name matched, and a second query over the same connection returned the new row again. The database layer was clean, which meant it was also the wrong place to keep digging.

The split inside a single render is the useful signal here. The footer and the JSON-LD block were not disagreeing about the data. They were disagreeing about when each had last read it. Whenever two consumers of one setting disagree inside the same response, the problem is almost never storage. It is freshness.

Where the value actually lives

The settings endpoint serves an in-memory snapshot built when the process boots. Any key the snapshot does not know falls back to a default, silently, with no warning in the response. A direct SQL write updates the table but never passes through the code path that rebuilds the snapshot, so the endpoint keeps answering from memory until the process restarts.

That contract is documented behavior, not a defect. Caching frameworks make the same trade-off explicit: Spring exposes population and eviction as separate, deliberate annotations, @Cacheable and @CacheEvict [3], so a write that skips eviction is visibly incomplete. Django's cache framework defines caching as saving "the result of an expensive calculation so that you don't have to perform the calculation next time" [4], which is exactly the deal: faster reads in exchange for a write path you must not skip. HTTP caching works the same way at the transport level, where a stored response is reused until it goes stale [1], and the React useState hook applies the same rule to the frontend, since its initialState argument is ignored after the initial render [2].

Read through that lens, a stale settings value is the cache doing precisely what it promised. Process managers that read an env file once at startup, config rendered at build time, and HTML held at an edge cache are the same shape one layer out.

Proving it across the layers

Confirm the row first, then the endpoint, then the rendered page. The mismatch appears between layers, not inside one of them.

# 1. make sure the settings row was actually written
psql "$DATABASE_URL" \
  -c "select key, value, updated_at from app_settings where key = 'site_name';"

# 2. compare with what the API serves
curl -s https://example.com/api/settings | python3 -m json.tool

If the query shows the new value while the API still prints the old one, the store and the reader have diverged; the problem sits between them, not in the database. If the endpoint is already correct but the page is not, the remaining suspects are the render layer or the client-side default fallback.

# 3. restart the API process, then repeat step 2
curl -s https://example.com/api/settings | python3 -m json.tool

After the restart the new value appears in the JSON, with not a single data change behind it. That confirmation is the boot-time cache holding the old value. If the page is served through an edge cache or a build step, that layer needs its own refresh too.

The rule adopted after this incident: after any direct write to settings, restart the API, and re-curl the endpoint before trusting what a server-rendered page shows. Verify from the bottom layer up, and never conclude from a single layer. A correct row is not evidence that anything reading it is correct.

Sources

  1. HTTP Caching, MDN Web Docs (accessed 2026-11-06)
  2. useState, React documentation (accessed 2026-11-06)
  3. Cache Abstraction annotations, Spring Framework documentation (accessed 2026-11-06)
  4. Django's cache framework, Django documentation (accessed 2026-11-06)

Related articles