Knex.js
Node.js
query resolution
database connection
graceful shutdown

Knex.js - node process never exits, how to close it gracefully - but only when all queries are resolved?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When a Node.js script using Knex finishes its queries but the process still does not exit, the usual reason is the open connection pool. Knex keeps database connections alive for reuse, and as long as that pool remains active, the event loop still has work to watch.

Why The Process Stays Alive

Knex is built on top of a pool manager. Even if every query promise has resolved, the pool itself still owns sockets and timers. That means Node sees active resources and does not terminate naturally.

The fix is not process.exit(). The fix is to wait for your query work to finish and then destroy the Knex instance cleanly.

The Basic Pattern

javascript
1const knex = require("knex")({
2  client: "sqlite3",
3  connection: {
4    filename: ":memory:",
5  },
6  useNullAsDefault: true,
7});
8
9async function main() {
10  await knex.schema.createTable("users", (table) => {
11    table.increments("id");
12    table.string("name");
13  });
14
15  await knex("users").insert({ name: "Ada" });
16  const rows = await knex("users").select("name");
17  console.log(rows);
18}
19
20main()
21  .catch((err) => {
22    console.error(err);
23    process.exitCode = 1;
24  })
25  .finally(async () => {
26    await knex.destroy();
27  });

The important line is await knex.destroy(). That closes the pool after the outstanding work is done.

Wait For All Queries Before Destroying

If you fire multiple queries concurrently, make the wait explicit with Promise.all or an equivalent coordination point.

javascript
1async function loadData() {
2  const [users, posts] = await Promise.all([
3    knex("users").select("id", "name"),
4    knex("posts").select("id", "title"),
5  ]);
6
7  return { users, posts };
8}
9
10loadData()
11  .then((data) => console.log(data))
12  .finally(async () => {
13    await knex.destroy();
14  });

Destroying the pool too early can interrupt in-flight queries, so the shutdown point belongs after the promises you care about have settled.

CLI Script Versus Long-Running Server

In a short-lived migration script, batch job, or one-off CLI program, destroying the Knex instance at the end is correct and necessary.

In a long-running web server, you usually keep Knex alive for the life of the process and destroy it only during shutdown.

javascript
1process.on("SIGTERM", async () => {
2  try {
3    await knex.destroy();
4  } finally {
5    process.exit(0);
6  }
7});

That pattern is for application shutdown, not for ending every request.

Avoid process.exit() As Normal Flow Control

process.exit() forces termination immediately and can cut off logs, pending writes, or cleanup steps. If you call it before the pool is destroyed, you are skipping the cleanup rather than solving the underlying issue.

A better pattern is:

  • await the real application work
  • destroy Knex
  • let Node exit naturally, or set process.exitCode if needed

Transactions Need The Same Discipline

If you use transactions, make sure the transaction callback has fully completed before shutdown begins.

javascript
1await knex.transaction(async (trx) => {
2  await trx("users").insert({ name: "Grace" });
3  await trx("logs").insert({ message: "user created" });
4});

Only after the transaction promise resolves should the pool be destroyed.

Common Pitfalls

The most common mistake is expecting resolved query promises to close the connection pool automatically. Another is calling knex.destroy() before all concurrent queries or transactions finish. Developers also sometimes add process.exit() because the script hangs, which masks the real problem and can cut off cleanup. In server applications, the opposite mistake appears too: destroying the shared Knex instance after one request and then wondering why later requests fail.

Summary

  • A hanging Node process after Knex work usually means the connection pool is still open.
  • Use await knex.destroy() after the queries you care about have resolved.
  • Coordinate concurrent queries with Promise.all before shutdown.
  • Use shutdown hooks for long-running servers, not per-request teardown.
  • Avoid process.exit() as a substitute for proper resource cleanup.

Course illustration
Course illustration

All Rights Reserved.