Introduction

Node.js caches modules loaded with require() -- the first call to require('./config') loads and executes the file, but subsequent calls return the cached exports without re-reading the file. This caching is essential for performance but causes issues in development servers, configuration reloaders, and plugin systems where file changes should be reflected without restarting the process. When the require cache is not invalidated, code changes are silently ignored, leading to confusion about why updated code is not taking effect.

Symptoms

File is edited but old code runs:

```javascript // config.js (initially) module.exports = { port: 3000 };

// Server loads config const config = require('./config'); console.log(config.port); // 3000

// Developer edits config.js to port: 4000 // Server reloads (without restart) const config2 = require('./config'); console.log(config2.port); // Still 3000! - cached version ```

Or in a plugin system:

```javascript // Plugin updated on disk fs.writeFileSync('./plugins/auth.js', newCode);

// But the old plugin code still runs const auth = require('./plugins/auth'); auth.verify(token); // Uses old logic ```

Common Causes

  • require() returns cached module: Node.js caches modules by resolved path
  • File path resolved differently: ./config and /abs/path/config are different cache entries
  • Native modules cannot be unloaded: .node addons are never uncachable
  • Circular dependencies complicate cache deletion: Deleting one module in a circular chain breaks the chain
  • ES modules (import) cannot be cleared: import does not have a cache API like require
  • Webpack/Bundler cache: Build tools have their own cache layers

Step-by-Step Fix

Step 1: Delete from require.cache

```javascript function clearRequireCache(modulePath) { const resolvedPath = require.resolve(modulePath); delete require.cache[resolvedPath];

// Also clear any child modules that were loaded by this module Object.keys(require.cache).forEach((key) => { if (key.startsWith(resolvedPath)) { delete require.cache[key]; } }); }

// Usage const config1 = require('./config'); console.log(config1.port); // 3000

clearRequireCache('./config');

const config2 = require('./config'); // Re-reads from disk console.log(config2.port); // 4000 (new value) ```

Step 2: Auto-reload with file watcher

```javascript const fs = require('fs'); const path = require('path');

function createReloader(modulePath) { const fullPath = require.resolve(modulePath);

function load() { clearRequireCache(modulePath); return require(modulePath); }

// Watch for file changes fs.watchFile(fullPath, { interval: 500 }, () => { console.log(${path.basename(fullPath)} changed, reloading...); try { const freshModule = load(); console.log('Reloaded successfully');

// Notify listeners of the change if (global.reloadListeners) { global.reloadListeners.forEach(fn => fn(freshModule)); } } catch (err) { console.error('Reload failed:', err.message); } });

return load(); }

// Usage const config = createReloader('./config'); ```

Step 3: Use dynamic import for ES modules

javascript // For ES modules (which cannot use require.cache) async function loadModule(filePath) { // Add cache-busting query parameter const timestamp = Date.now(); const module = await import(${filePath}?t=${timestamp}`); return module.default; }

// Usage in an ES module project const freshConfig = await loadModule('./config.js'); ```

Prevention

  • Always use delete require.cache[require.resolve(path)] to invalidate cached modules
  • Use file watchers in development servers for automatic module reloading
  • Consider using nodemon or ts-node-dev for development auto-restart
  • For production, restart the process instead of hot-reloading modules
  • Use dynamic import() for ES module hot-reloading with cache-busting query parameters
  • Log cache invalidation events to track which modules are being reloaded
  • Add a /reload admin endpoint for configuration hot-reloading in production