Stop paying Neon to stay awake for work nobody asked for - #115
Merged
Merged
Conversation
Neon bills compute by the hour it is awake, and an idle compute suspends after its autosuspend window. That makes every avoidable query worse than it looks: it is not the query that costs, it is the wake-up, which bills for the whole window whether or not there was anything to do. Four changes, all aimed at keeping the compute asleep: - The lease-expiry cron drops from hourly to daily. Hourly was 24 wake-ups a day -- most of a near-idle month's compute -- for work reclaimLapsedLocks() had almost always already done on the hot path. The lazy sweep is the real mechanism: a stranded reservation is reclaimed by the next pool read, and the only thing a pool read waits on is a volunteer who wants work. The cron is the floor under a system nobody is touching, where a blocked budget harms nobody. - /health no longer runs SELECT 1 by default. An uptime monitor on a 1-minute interval would hold the compute awake permanently and outspend all real traffic; the DB probe moves behind ?db=1 for when you actually want it. - Public reads (/leaderboard, /transparency, /tasks/available, and the per- conjecture tree + contributions) carry a shared-cache Cache-Control, so Cloudflare answers ordinary site traffic without touching Postgres. Successes only -- caching an error would pin a transient 500 at the edge for its full s-maxage. - The runner's poll interval defaults to 60s instead of 15s. Four idle polls a minute held the compute awake for a whole watch session, and a volunteer waiting up to a minute longer costs nothing when tasks run for minutes. test/public-cache.test.ts holds the rule both ways: public reads are cacheable, caller-specific reads and error responses never are. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoAJRWLbAW5pakUUzEzcjV
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.
Hit a $2 spend alert on Neon. Neon bills compute by the hour it is awake, and an idle compute
suspends — so the thing to minimize is not queries, it is wake-ups. Each one bills for the
whole autosuspend window whether or not there was anything to do.
Four changes, all aimed at keeping the compute asleep.
1. Lease-expiry cron: hourly → daily
crons = ["0 * * * *"]→["17 4 * * *"].Hourly was 24 wake-ups a day ≈ 2 compute-hours/day ≈ ~15 CU-h/month at 0.25 CU — plausibly most of
the bill — for work
reclaimLapsedLocks()(src/operations.ts:418) had almost always already doneon the hot path.
The lazy sweep is the real mechanism: a stranded reservation is reclaimed by the next pool read, and
the only thing a pool read can be waiting on is a volunteer who wants work — exactly when it
matters. The cron is the floor under the case where nobody touches the system at all, where a
blocked budget also harms nobody until someone shows up.
2.
/healthno longer hits the DB by defaultIt ran
SELECT 1. Any uptime monitor on a 1-minute interval would hold compute awake permanentlyand outspend all real traffic combined. The probe moves behind
?db=1.If you have a monitor pointed at
/health, it keeps working unchanged — it just stops assertingthe DB. Point it at
/health?db=1only if you want readiness, and only on a slow interval.3. Shared-cache headers on public reads
/leaderboard,/transparency,/tasks/available,/conjectures/:slug/tree,/conjectures/:slug/contributionsnow sendpublic, max-age=60, s-maxage=300, stale-while-revalidate=600, so Cloudflare's edge answers sitetraffic without touching Postgres. These pages are fetched on every visit to the marketing site.
Successes only — caching an error would pin a transient 500 at the edge for the full
s-maxage.Anything behind
requireDev/requireAdminstays uncached.4. Runner poll interval: 15s → 60s
Four idle polls a minute held compute awake for an entire
--watchsession. A volunteer waiting upto a minute longer costs nothing when the tasks themselves run for minutes.
--intervalstilloverrides.
Tests
test/public-cache.test.tsholds the rule in both directions: public reads carrys-maxage;caller-specific reads (
/budget) and error responses never do. Full suite 48 files / 603 tests,typecheck and Biome clean.
Not in this PR — console-side, needs your hand
probably the single biggest remaining lever.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WoAJRWLbAW5pakUUzEzcjV