What happened
On 2026-10-03, creating apps on carloku failed for everyone. The browser form said "Could not check that name. Please try again." The CLI said "could not create the app — try again". Users who retried with the CLI then hit "app creation limit reached for today", because every failed attempt counts against the 10-per-day create budget.
The console log on the flagship IE box (build 7edd4b33) shows the cause, starting at 16:23:35 UTC and repeating until the console was restarted at 18:54 UTC:
console: api routes: registry list paths: sql: database is closed
Restarting console.service fixed it, but it will happen again the next time an operator runs carlos status --instances.
Cause
openRegistry (internal/console/view.go:506) caches one *registry.Registry per path for the life of the process. Every caller shares that handle.
handleAPIStatusInstances (internal/console/apistatusinstances.go:117, still on main) gets the shared handle through openPassRegistry and then closes it:
reg, err := s.openPassRegistry() // returns the cached, process-wide handle
...
defer reg.Close()
After one carlos status --instances call, every later registry read in that console process fails with sql: database is closed. The registry is read when creating an app (apexHolder in apex.go), listing routes, managing domains, loading the fleet and account review pages, and in the CLI's config and create paths.
The failure was hard to diagnose because claimApp (internal/console/server.go, around lines 3455–3510) returns claimCheckFailed without logging the error in all three places it can fail:
- the
HasAppIn lookup - the
HasOwners lookup - a non-bucket error from
apexHolder
The CLI handler's comment says "the detail stays on the box", but for this outcome nothing is kept anywhere.
Fix
- Remove
defer reg.Close() from handleAPIStatusInstances. Add a regression test: call the status-instances handler, then check that a later openRegistry read still works. - Make it impossible to close the shared handle by accident. For example,
openRegistry could return a wrapper whose Close does nothing, or the cache could hand out a type that has no Close method. Any future defer reg.Close() would then fail to compile or be harmless. - Log
err with the account and app name on each claimCheckFailed return in claimApp, so the next failure here can be seen in the console log.
Side effect worth knowing
The create budget is spent per attempt, before the name is read. When the console itself is broken, users use up their daily budget on retries that could never succeed. The budget is held in memory, so a console restart resets it. Consider not charging the budget for claimCheckFailed or claimStoreFailed outcomes, which are server faults, not user actions.