~/.portless/routes.json can end up corrupted or emptied when a client is killed before its exit cleanup finishes
Author: jelicalCreated Aug 13, 2026Updated Sep 2, 2026
Summary
Running a dev script through portless from an IDE run/debug configuration (e.g. WebStorm) spawns a chain like WebStorm -> yarn -> portless -> next dev. Stopping the debug session kills that whole tree. Instead of just that one route entry being cleared, ~/.portless/routes.json is sometimes left corrupted (invalid JSON) or completely emptied — other apps' routes disappear too, not just the one that was stopped.
Reproduction
package.json:{"scripts": {"dev": "portless myapp next dev"}}- Run it via an IDE debug configuration (or anything that spawns portless through an intermediate shell/package-manager process and force-kills the whole tree on stop).
- Stop the debug session a handful of times.
cat ~/.portless/routes.json— occasionally invalid JSON, or[]even though other apps are still running.
Root cause
Two independent issues compound:
RouteStore.saveRoutes(packages/portless/src/routes.ts) writesroutes.jsonin place with a singlefs.writeFileSync. That's not atomic — a process killed mid-write()leaves a truncated file.loadRoutesthen failsJSON.parse, logs "Corrupted routes file", and returns[].- The exit-time cleanup path (
spawnCommand's SIGINT/SIGTERM handler incli-utils.ts,runApp'sonCleanupincli.ts) removes the route synchronously, includingRouteStore.acquireLock(), which retries via a genuinely blockingAtomics.waitfor up to 15 seconds under lock contention. An IDE's stop/debug action typically escalates from SIGTERM/SIGINT to an uncatchable SIGKILL after a short grace period. If that escalation lands mid-write or mid-lock-wait, the process dies mid-operation instead of completing or safely aborting.
Together: a slow/interrupted cleanup plus a non-atomic write turns "one dead client's route should be removed" into "the whole file is corrupted."
Suggested fix
- Make
saveRoutesatomic: write to a temp file in the same directory, thenfs.renameSyncover the target, so an interruption at any point leaves either the old file or the new one intact, never a torn one. - Have the proxy itself periodically sweep dead-PID routes (it already has
pruneStaleRoutes(), used today only byportless prune), rather than relying solely on the exiting client's own handler completing.
Happy to open a PR for both.
Source: vercel-labs/portless