Zombie processes eating your port.
That’s what happens when you pair Turso with nodemon and forget one critical detail. You write new code, save the file, and… nothing. The old route doesn’t update. The console screams EADDRINUSE: address already in use :::3000. Welcome to a very specific, very annoying development trap that catches most developers using libSQL or Turso at least once.
The problem is architectural, not accidental. And once you understand why it happens, the fix is embarrassingly simple.
Why Your Database Client Won’t Let Go
Here’s the thing: @libsql/client opens a persistent socket connection to your Turso database. That connection stays open. It’s a good design choice—it makes queries fast. But Node.js has a rule: if any background process is still running, the entire application refuses to exit.
To make queries fast, the @libsql/client holds a persistent, open socket connection to your Turso database. Because Node.js is designed to never exit as long as there is an active background process (like a database connection), whenever nodemon tries to restart your app, the old process refuses to die.
So when nodemon sends a restart signal, your Express server shuts down fine. But that database socket? Still there. Still listening. The old Node process becomes a ghost—a zombie—hanging onto Port 3000, preventing your fresh code from binding to it.
You can write all the new routes you want. They’ll never load because the dead process won’t budge.
Is This a Turso Problem or a Nodemon Problem?
Neither, really. It’s a signal handling problem. Nodemon sends SIGUSR2 to restart your app. Your app listens for it… but it only half-shuts down. The database connection never gets told to close.
This isn’t unique to Turso, by the way. Any persistent database client—Redis, PostgreSQL with pg-promise, MongoDB drivers—can trap you the same way if you don’t handle graceful shutdown properly. Turso just makes the pattern more visible because the socket stays aggressively open.
The architectural lesson here: persistent background connections need explicit cleanup logic. You can’t assume they’ll just vanish when your server stops listening.
The Fix: Signal Handlers That Actually Work
You need to intercept the shutdown signal and close your database connection before the process exits. Here’s the pattern:
First, grab a reference to your running server instead of just calling app.listen() and walking away.
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Then, at the bottom of your entry file, add a shutdown handler:
import { tursoClient } from "./config/turso.js";
const shutdown = async () => {
console.log("Shutting down gracefully...");
tursoClient.close();
server.close(() => {
process.exit(0);
});
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.once("SIGUSR2", async () => {
await shutdown();
});
This catches three termination scenarios: Ctrl+C in your terminal (SIGINT), Docker/system shutdown (SIGTERM), and nodemon restarts (SIGUSR2). The database closes first. The server closes second. The process exits third. Nodemon sees the exit and spawns a fresh instance.
Clean. Predictable. No zombies.
Why This Matters Beyond Nodemon
There’s a deeper lesson embedded here. Most developers treat graceful shutdown as optional—something you add when you feel like it. But in a world where databases are separate services, where containerization is standard, where you might swap your database vendor mid-project, signal handling is infrastructure.
If you deploy this app to Docker and kill the container, the same problem occurs. If you run it on a PaaS that sends SIGTERM before tearing down your dyno, same thing. The zombie process now costs you production outages instead of dev-time frustration.
Building the shutdown logic early isn’t a “nice to have.” It’s part of writing resilient Node.js applications.
The Nuclear Option (When Zombies Already Exist)
If you’ve been running your app without this fix, you probably have orphaned processes still gripping your port. Kill them before running nodemon again.
On Windows (PowerShell):
npx kill-port 3000
On Mac/Linux:
killall node
Then start fresh with npm run dev. Every save triggers a clean shutdown and restart now.
What This Reveals About Development Workflow
The fact that this trips up so many developers says something about how we onboard with new tools. When you scaffold a new Turso project, the docs show you how to query the database. They rarely show you the wiring required to make it play nicely with your development environment.
It’s not a documentation failure, exactly. It’s more that developers tend to rush past the “production-ready code” checklist when building locally. Graceful shutdown feels ceremonial until it isn’t.
The developers who get burned once and fix it? They rarely get burned again. The architecture sticks with them. And they write better deployment logic as a result.
🧬 Related Insights
- Read more: How TeamPCP’s Self-Propagating Worm Turned Open Source Into a Backdoor Factory
- Read more: Docker Sandboxes: How to Let AI Agents Run Wild Without Burning Your House Down
Frequently Asked Questions
Will killing the process harm my database? No. Turso is fully managed. Closing your client connection just tells the database, “I’m done.” It doesn’t corrupt anything. If you had in-flight transactions (which you shouldn’t in a dev environment), they’d rollback naturally.
Do I need to add this to every Express app? Only if you’re using a persistent database client. Simple apps with no background connections don’t need it. But if you’re using any real database—Turso, PostgreSQL, MongoDB, Redis—adding signal handlers is best practice.
Why doesn’t nodemon handle this automatically? Nodemon doesn’t know which processes are “yours” to close. It can’t introspect your database client. The shutdown logic has to live in your application code, not in the tooling layer.