← Alle Neuigkeiten
ops-basics4 Min. Lesezeit

Your AI-Assisted App Has a Dirty Little Secret: It Never Says Goodbye

Von Seaworthy · 4. November 2026

Overhead aerial view of colorful shipping containers stacked at a port with a cargo crane visible

Photo by Bernd Dittrich on Unsplash

Your AI-assisted app has a dirty little secret: it never says goodbye

I keep seeing the same pattern in codebases built with heavy AI assistance. The assistant writes a clean Express server, wires up a database pool, maybe adds a message queue consumer. It looks production-ready. Then you deploy, and everything falls apart in a way that is hard to diagnose: half-written rows, duplicate messages, connection pool exhaustion that only shows up after a few restarts.

The culprit is almost always the same: no graceful shutdown handling. The process just dies. SIGTERM arrives, the runtime exits, and whatever was in flight is lost. AI assistants are great at writing the happy path. They are less reliable at remembering that processes have a lifecycle beyond start.

What actually happens on shutdown

When you deploy to Kubernetes, ECS, or most modern platforms, the orchestrator sends SIGTERM to your process. You have a grace period (often 30 seconds) before SIGKILL. If you do nothing, your app is killed mid-request. Open database transactions get rolled back by the server, but any application-level state you wrote (partial files, in-memory counters you flush on exit, queue acknowledgements) is now inconsistent. Worse, load balancers may still be routing traffic to your instance for a few seconds after SIGTERM. A process that exits immediately drops those requests.

I have watched teams spend days chasing "random" 502s during deploys that were simply a missing signal handler. The fix takes twenty minutes. The debugging takes far longer.

The pattern I keep finding

Here is the shape of the code an assistant will happily generate:

const express = require('express');
const { Pool } = require('pg');

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const app = express();

app.get('/orders/:id', async (req, res) => {
  const result = await pool.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);
  res.json(result.rows[0]);
});

app.listen(3000, () => console.log('listening on 3000'));

Ask yourself what happens when this process gets SIGTERM. Nothing. The default behavior kills it. In-flight queries are abandoned. The pool never drains. If you run a job consumer alongside this, messages get redelivered or lost depending on your broker's semantics.

A minimal fix

The core idea: stop accepting new work, finish what is in flight, close your resources, then exit. And do it with a timeout so a hung request does not block the deploy forever.

const express = require('express');
const { Pool } = require('pg');

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const app = express();

app.get('/orders/:id', async (req, res) => {
  const result = await pool.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);
  res.json(result.rows[0]);
});

const server = app.listen(3000, () => console.log('listening on 3000'));

let shuttingDown = false;

async function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(`received ${signal}, shutting down`);

  const forceExit = setTimeout(() => {
    console.error('shutdown timed out, exiting hard');
    process.exit(1);
  }, 25000);
  forceExit.unref();

  server.close(async (err) => {
    if (err) console.error('server close error', err);
    try {
      await pool.end();
      console.log('pool drained, exiting cleanly');
      process.exit(0);
    } catch (e) {
      console.error('pool close error', e);
      process.exit(1);
    }
  });
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

A few details matter here. server.close stops new connections but lets in-flight requests finish. The timeout is a backstop. pool.end() waits for checked-out clients to return. And the shuttingDown guard prevents double-shutdown races if both SIGTERM and SIGINT arrive.

Things the assistant will forget

Even with a handler in place, a few gotchas bite:

  • Readiness probes. If your app reports ready until the process dies, the load balancer keeps sending traffic during the drain. Flip readiness to false when shutdown starts, then wait a few seconds before closing the server.
  • Background workers. A setInterval job or a queue consumer needs its own stop logic. Closing the HTTP server does nothing for them.
  • Long-lived connections. WebSockets and server-sent events will keep server.close waiting. Track them and close them explicitly, or your 25-second timeout becomes the normal path.
  • Log flushing. If you buffer logs, flush before exit. Otherwise your last, most interesting log lines vanish.

My take

This is not a niche concern. Every deploy exercises shutdown. Every autoscale event exercises shutdown. AI assistants optimize for the code you see in a tutorial, and tutorials rarely show process.on('SIGTERM'). That means the burden is on you to review generated code for lifecycle concerns, not just feature correctness. Add "does this handle SIGTERM?" to your mental diff checklist. It is a cheap question with an outsized payoff.

If you want to see how a given service behaves under shutdown, Seaworthy runs black-box checks that include sending SIGTERM and observing whether the process drains cleanly before its timeout.

Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.

Von den Machern von Seaworthy

Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.

Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.

$ npx seaworthycode