blog: serve last known good instead of 500 when SQLite Cloud blinks - #18
Merged
Conversation
c0mpute.com/blog and /blog/rss.xml intermittently answer 500 — observed returning 500 and 200 minutes apart with no deploy in between, sometimes the page failing while the feed succeeded and sometimes the reverse. listPosts() already retries once through withReconnect(), but when the retry fails too the error reaches the route and Next answers a bare 500. That matters most for the feed. A blog page that fails is a page someone reloads; a feed that 500s is one every reader eventually stops polling and treats as dead, and this feed is listed in profullstack.com/feeds.opml and in the smallweb list. So reads go through readCached(), which keeps a last-known-good copy and serves it when the database cannot be reached. An outage now degrades to slightly stale posts rather than to no posts. When nothing has ever succeeded the error is still thrown, because an empty feed served as 200 would claim the blog has no posts at all. The copy is kept in process, not only in Redis. REDIS_URL is referenced exactly once in this repository — by getRedis() — and set nowhere: no .env.example, no deploy config, no documentation. Every Redis path here is already a no-op in production, so a fallback that lived only in Redis would never once be read. `next start` is long-running, so a Map covers precisely the failure this is for. The Redis copy is written too, under `blog:stale:` rather than `blog:list:`, because cacheDelListKeys() wipes the latter on every publish and would throw the safety net away at the moment the site is being written to. Used only on the failure path, never as the happy-path cache: publishing invalidates through Redis, so an in-process fresh cache would hide new posts for a full TTL. getPost() is left alone — it deliberately does not cache misses, and a single post page failing does not make a feed look dead. Verified: tsc --noEmit clean, next build succeeds, blog routes still dynamic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan8 finding(s) HIGH/CRITICAL: 5 | MEDIUM: 3
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 13, 2026
No Rust changed since v0.2.25. Every commit in this range is web, CI or docs, so the binaries this tag builds are identical to the previous ones and the version marks the site rather than the client: - blog: serve last known good instead of 500 when SQLite Cloud blinks (#18) - ci: add ThreatCrush security scan (#17) - feat(web): WebMCP callout on the homepage (#16) - docs(web): document WebMCP tools (#15) - feat(web): WebMCP support on c0mpute.com (#14) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The symptom
c0mpute.com/blogand/blog/rss.xmlintermittently answer 500. Observed today with no deploy in between:/blog/blog/rss.xmlSo it is transient, and either route can be the one that fails.
listPosts()already retries once viawithReconnect(), but when the retry fails too the error reaches the route and Next answers a bare 500.Why it matters more for the feed
A blog page that fails is a page someone reloads. A feed that 500s is one every reader eventually stops polling and treats as dead — and this feed is listed in
profullstack.com/feeds.opmland in the smallweb list, so the failure is visible to subscribers rather than just to visitors.The change
Reads go through
readCached(), which keeps a last-known-good copy and serves it when the database cannot be reached. An outage degrades to slightly stale posts instead of no posts.When nothing has ever succeeded, the error is still thrown — an empty feed served as 200 would claim the blog has no posts, which is worse than an honest failure.
Two details that are load-bearing:
REDIS_URLis referenced exactly once in this repository — bygetRedis()— and is set nowhere: no.env.example, no deploy config, no docs. Every Redis path here is already a no-op in production, so a fallback that lived only in Redis would never once be read.next startis long-running, so aMapcovers exactly this failure. The Redis copy is written too, for when it is configured.blog:stale:, notblog:list:.cacheDelListKeys()wipesblog:list:*on every publish, which would throw the safety net away at the precise moment the site is being written to.The in-process copy is used only on the failure path, never as the happy-path cache: publishing invalidates through Redis, so an in-process fresh cache would hide new posts for a full TTL.
getPost()is left alone — it deliberately does not cache misses, and a single post page failing does not make a feed look dead.Root cause, not fixed here
This is SQLite Cloud refusing connections. c0upons hit the same provider's limits and migrated to Turso. This PR makes the blog survive the blips; it does not change the database. Worth deciding separately whether c0mpute follows c0upons off SQLite Cloud.
Verification
tsc --noEmitclean.next buildsucceeds;/blog,/blog/[slug]and/blog/rss.xmlare still dynamic.🤖 Generated with Claude Code