DEV Community

Alexey Yakovlev
Alexey Yakovlev

Posted on • Edited on

2 1 1 1

How to Cache Variables Between Restarts in Your Node.js Application

Hot reloading is a staple of Node.js development: when a file changes, your application restarts, re-running all setup logic. For small projects, this is barely noticeable. But for large monoliths, especially those with heavy startup routines, the wait can be frustrating.

This was exactly my situation. My application used webpack-dev-middleware to compile a massive monolithic project with thousands of modules. Each restart triggered a full Webpack build, which, thanks to legacy loaders, could take up to 70 seconds. On top of that, one server module fetched a 60MB JSON configuration from an external source, and parsing this data took another 90 seconds. Every minor change meant waiting a minute and a half before I could see the results—completely killing my development feedback loop.

Why Not Just Optimize?

I considered several approaches:

  • Load configuration on-demand:

    Not feasible. The configuration was deeply coupled and used everywhere, so lazy loading wasn’t practical.

  • Decouple configuration-dependent modules:

    This would only help for modules that didn’t need the config, but most did.

  • Optimize load times:

    We were already transitioning to Vite, but the bottleneck was the unavoidable JSON.parse on a huge file.

  • Cache heavy operations between reloads:

    This seemed the most promising: minimal effort, maximum impact.

But how do you cache variables between restarts?

Webpack HMR and why it did not work for me

Webpack implements its own module system, enabling Hot Module Replacement (HMR) in both browser and Node.js environments. In development, you can use module.hot to keep state between reloads:

const expensiveVariable = module.hot?.data?.expensiveVariable ?? createExpensiveVariable();
if (module.hot) {
  module.hot.dispose((data) => {
    data. expensiveVariable = expensiveVariable;
  });
}
Enter fullscreen mode Exit fullscreen mode

However, this requires compiling your app with Webpack, which is slow and complex for large backends. I deemed this completely unnecessary for my plain JavaScript backend. Other tools like Babel or SWC don’t provide HMR for Node.js.

The CommonJS Trick: Keeping Variables Between Restarts

Since my app was written in CommonJS, I could leverage Node’s require.cache API. This cache tracks all loaded modules. By clearing the cache for everything except a special “keeper” module, I could persist variables across reloads. This is not possible with ES Modules, as they don’t expose a public cache API.

The Hot-Reload Entrypoint

To make this work, I needed a new entrypoint that:

  • Watches source files for changes
  • Clears the require cache for changed files
  • Restarts the application in the same process

Killing and respawning a child process wouldn’t work, since each process has its own memory and cache. Instead, the entrypoint must control the server and be able to close and restart it in-place.

Here’s a simple implementation:

const fs = require('fs');
const http = require('http');
const { createRequire } = require('module');
const chokidar = require('chokidar');

const requireApp = createRequire(__filename);
let server = null;

function startServer() {
  if (server) server.close();
  const app = requireApp('./app.js');
  server = http.createServer(app).listen(3000);
}

chokidar.watch('./src').on('change', (file) => {
  Object.keys(require.cache).forEach((key) => {
    if (key.includes('/src/')) delete require.cache[key];
  });
  startServer();
});

startServer();
Enter fullscreen mode Exit fullscreen mode

You could optimize further by only clearing changed modules and their dependents, but this is more complex and not always faster.

The Variable Cache Module

To persist variables, I created a special module that is never cleared from the cache. It simply exports keep and kept functions:

// keeper.js
const store = new Map();

function keep(name, value) {
  store.set(name, value);
  return value;
}

function kept(name, defaultValue) {
  return store.has(name) ? store.get(name) : defaultValue;
}

module.exports = { keep, kept };
Enter fullscreen mode Exit fullscreen mode

In the app, it is used like this:

const { keep, kept } = require('./keeper');

// On startup, restore or create the expensive resource
let expensiveResource = kept('expensiveResource', null);
if (!expensiveResource) {
  expensiveResource = createExpensiveResource();
  keep('expensiveResource', expensiveResource);
}
Enter fullscreen mode Exit fullscreen mode

Now, when the app reloads, the resource is kept alive!

Forcibly Closing Connections

One last challenge: some middlewares (like webpack-dev-middleware or webpack-hot-middleware) keep connections open, preventing the server from closing. To fix this, the entrypoint must forcibly close all open sockets:

const sockets = new Set();

server.on('connection', (socket) => {
  sockets.add(socket);
  socket.on('close', () => sockets.delete(socket));
});

function closeServer() {
  for (const socket of sockets) {
    socket.destroy();
  }
  server.close();
}
Enter fullscreen mode Exit fullscreen mode

Probably those middlewares should handle close calls themselves and destroy active connections, but they did not.

The Result

With this setup, I can now restart my server almost instantly after a file change, instead of waiting 90 seconds or more. The feedback loop is tight, and development isn't painful anymore.

Introducing: hot-keeper

I’ve packaged this approach into a reusable CLI utility: hot-keeper.

Install it from npm:

pnpm add hot-keeper
Enter fullscreen mode Exit fullscreen mode

Use it as your entrypoint:

pnpm hot-keeper app.js
Enter fullscreen mode Exit fullscreen mode

And in your app, use the cache helpers:

const { keep, kept } = require('hot-keeper');

let expensive = kept('expensive', null);
if (!expensive) {
  expensive = createExpensive();
  keep('expensive', expensive);
}
Enter fullscreen mode Exit fullscreen mode

For further configuration and more examples, see the README.

Now you can enjoy fast reloads regardless of setup routines in your Node.js CommonJS projects!

Top comments (0)