Skip to content
Warlock.js v4

Shutdown & flushing

FileLog and JSONFileLog buffer entries in memory before writing them to disk. This keeps the logger fast and non-blocking — but it means an ungraceful shutdown can drop the last few entries. This page covers when writes happen, what can be lost, and how to guarantee durability.

For FileLog and JSONFileLog, every log() call adds an entry to an in-memory buffer. A write is triggered when any of these hold:

  1. The buffer reaches maxMessagesToWrite (default 100).
  2. More than 5 seconds have elapsed since the last write — checked both immediately after each log() call and by a 5-second background interval.
  3. You call log.flushSync() explicitly.

The first two are asynchronous (Node streams). The third is synchronous (fs.appendFileSync).

Anything in the buffer when the process terminates without a synchronous flush:

Exit pathBuffered entries lost?
Normal process.exit()Yes — unless you called log.flushSync() first
Uncaught exception without a handlerYes
SIGINT / SIGTERM without a handlerYes
OS-level kill (kill -9, OOM)Yes — unavoidable
await log.info(...) then process.exit()Yeslog.info resolves once the entry is buffered, not written

The asynchronous 5-second flush provides eventual durability while the process is alive. It does not help at shutdown.

Pass autoFlushOn to configure() and the logger installs the handlers for you:

src/logger.ts
import { log, ConsoleLog, FileLog } from "@warlock.js/logger";
log.configure({
channels: [new ConsoleLog(), new FileLog({ chunk: "daily" })],
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
});

Every buffered channel drains before the process exits. You can also call log.enableAutoFlush([...]) directly at any point, and log.disableAutoFlush() to remove every installed handler. Calling enableAutoFlush twice replaces the previous handlers (no stacking).

What each event does:

EventBehavior
SIGINT / SIGTERM / SIGHUP / SIGBREAK / SIGUSR2Flush, then re-raise the signal so Node’s default exit behavior runs (exit code 128 + signal number, same as if you had no handler).
beforeExitFlush in place. Node exits on its own afterwards — no re-raise.

If you need to run async work before flushing (closing an HTTP server, finishing an outbound batch), register your own handler and call log.flushSync() at the end:

src/shutdown.ts
import { log } from "@warlock.js/logger";
async function gracefulShutdown() {
await httpServer.close();
log.flushSync();
process.exit(0);
}
process.once("SIGINT", gracefulShutdown);
process.once("SIGTERM", gracefulShutdown);

flushSync() is fan-out: it calls flushSync() on every registered channel that implements it. ConsoleLog has no buffer and is skipped; FileLog and JSONFileLog each drain to disk. Channels without flushSync are ignored silently.

If you use captureAnyUnhandledRejection(), include "beforeExit" in autoFlushOn so the terminal error entry reaches disk before Node tears down:

src/index.ts
import {
log,
ConsoleLog,
FileLog,
captureAnyUnhandledRejection,
} from "@warlock.js/logger";
log.configure({
channels: [
new ConsoleLog(),
new FileLog({ chunk: "daily", levels: ["error", "warn"] }),
],
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
});
captureAnyUnhandledRejection();

Without "beforeExit" (or an equivalent manual handler), a crash logs the error into the buffer, then the process exits before the 5-second flush interval fires.

Channels that don’t buffer (like ConsoleLog) skip the call. If every registered channel is unbuffered, flushSync() returns immediately — safe to call unconditionally. Within a FileLog / JSONFileLog, flushSync() is also a no-op when the buffer is empty, so you can schedule it liberally without redundant writes.