Skip to content

fix(cost): widen reservation sweep to hourly so Neon can auto-suspend - #123

Merged
bgard68 merged 3 commits into
mainfrom
fix/reservation-sweep-compute
Sep 14, 2026
Merged

bgard68 merged 3 commits into
mainfrom
fix/reservation-sweep-compute

Conversation

@bgard68

@bgard68 bgard68 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Stops WidgetWorks from burning through the Neon free compute allowance (project hit 80% of 100 CU-hrs mid-month).

Root cause

ReservationSweeper queried Postgres every SweepIntervalMinutes = 5. Neon compute auto-suspends after ~5 min idle, so a query every 5 minutes reset the idle timer right before it could sleep — holding the database awake ~24/7 (~180 CU-hrs/month, nearly double the free tier). keep-warm is not the cause — it pings /health, which runs no DB query.

Fix (Option 1: slow the sweep)

Sweep hourly so Neon sleeps between passes. The repo enforces ExpireAfterMinutes > SweepIntervalMinutes (stock must be reclaimed faster than the window, or expired orders sit unreleased), and ShippedConfigurationTests requires appsettings to match the code defaults — so both the code defaults and appsettings move together, and the window is lengthened to keep the invariant:

setting before after
SweepIntervalMinutes 5 60
ExpireAfterMinutes 15 90
  • Neon compute: ~180 → ~15 CU-hrs/mo (~12× cut) — finishes the month well under 100 even after the ~82 already spent.
  • Behavior: abandoned-checkout stock is reclaimed in ~90–150 min instead of ~15–20. Harmless on this demo (no real inventory at stake); a production store with scarce stock would instead move to on-access release (kept as a noted follow-up).
  • Verified locally: ShippedConfigurationTests + ReservationSweeperTests pass (7/7).
  • Takes effect on the next API deploy. I did not touch billing or click "Upgrade".

taskboard (ToDoApp)

Checked — no background poller, keep-warm hits a no-DB endpoint, dapper doesn't deploy. Not at risk; no change needed.

🤖 Generated with Claude Code

bgard68 and others added 3 commits September 14, 2026 10:43
The ReservationSweeper queried Postgres every SweepIntervalMinutes=5. Neon's
compute auto-suspends after ~5 minutes idle, so a query every 5 minutes reset
the idle timer just before it could sleep — holding the database awake ~24/7.
At Neon's 0.25 CU floor that is ~180 CU-hrs/month, nearly double the 100 CU-hr
free allowance (the project hit 80% mid-month).

The sweep only reclaims stock from orders left in AwaitingPayment past
ExpireAfterMinutes (15) — pure housekeeping, not latency-sensitive. Widening it
to hourly lets Neon sleep between passes (~5 min awake per sweep, ~15 CU-hrs/mo,
a ~12x cut) while still returning abandoned-checkout stock within ~75 minutes.

No code change — config only. keep-warm still pings /health (no DB query), so
the API stays warm without holding the database open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…riant

The first pass changed only appsettings, which broke two guard tests:
ShippedConfigurationTests requires the file to match the code defaults, and
another test requires ExpireAfterMinutes > SweepIntervalMinutes (stock must be
reclaimed faster than the window or expired orders sit unreleased).

So the code defaults move with the file: SweepIntervalMinutes 5 -> 60 and
ExpireAfterMinutes 15 -> 90 (window stays above the interval). Comments updated
to explain both — the interval is set by Neon's ~5-min auto-suspend (a shorter
one pins the DB awake ~24/7), and the longer hold is harmless on this demo where
no real inventory is at stake. Verified locally: ShippedConfigurationTests and
ReservationSweeperTests pass (7/7).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Raising the default ExpireAfterMinutes to 90 (for Neon cost) broke handler
tests that park orders ~30 min ago and expect a release — correct against the
old 15-min default, stale-but-inside-window against 90. These are behaviour
tests of "past the threshold -> released", so they should fix their own window
rather than ride on the production default. Each now sets
ExpireAfterMinutes = 15 explicitly. Verified: WidgetWorks.UnitTests 369/369.
(The reservation/config guard tests in ApiTests already pass, 7/7.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bgard68
bgard68 merged commit a5cc6ad into main Sep 14, 2026
9 checks passed
@bgard68
bgard68 deleted the fix/reservation-sweep-compute branch September 14, 2026 16:04
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