NodeJSModule Caching

Module Caching

Node evaluates each module exactly once. The first require (or import) runs the file top to bottom and stores the result; every later request for the same module returns that cached value without re-running the code. This is not just an optimization — it is the mechanism behind the most common way to share state in Node: the module singleton.

The body runs once

setup.js

JS
console.log('setup.js is being evaluated')
module.exports = { ready: true }

app.js

JS
const a = require('./setup')
const b = require('./setup')
const c = require('./setup')

console.log(a === b, b === c)   // true true — same object every time
setup.js is being evaluated
true true

The log appears once despite three require calls. After the first, Node returns the cached module.exports instantly.

The cache key is the resolved path

Modules are cached by their absolute resolved filename, not the string you typed. So require('./utils') and require('./utils.js') from the same folder hit the same cache entry — but the same package resolved at two different node_modules depths counts as two separate modules:

JS
const Module = require('node:module')
console.log(Object.keys(require.cache))
// [ '/app/app.js', '/app/setup.js', ... ] — keyed by absolute path
Two copies of one package is a real bug source
If your dependency tree installs `lodash` at both `/app/node_modules` and `/app/node_modules/some-lib/node_modules`, those are *distinct* cache entries with *distinct* state. `instanceof` checks and module-level singletons silently break across them. Lockfiles and deduping (`npm dedupe`) exist partly to avoid this.
The singleton pattern (the good use)

Because a module is evaluated once and shared, anything you create at module scope becomes a process-wide singleton — the idiomatic, global-free way to share a database pool, a config object, or a client:

db.js — created once, shared everywhere

JS
import { Pool } from 'pg'

// This Pool is constructed the first time db.js is imported,
// then the SAME instance is returned to every other importer.
export const pool = new Pool({ connectionString: process.env.DATABASE_URL })

JS
// users.js and orders.js both import './db.js'
import { pool } from './db.js'   // identical shared pool — no global needed
Why this beats global state
The singleton is explicit (you can see the import), type-safe, and testable — none of the hidden-coupling problems of stashing things on `globalThis`. See [The global Object](/nodejs/global-object).
Caching and errors
A module that throws is NOT cached
If a module throws *during* its first evaluation, Node removes it from the cache, so the next `require` tries to run it again from scratch. This is why a failing module can re-execute its (partial) side effects on retry — keep module top-level code idempotent and side-effect-light.
Busting the cache (CommonJS)

Occasionally — usually in tooling or tests — you want to force a fresh load. In CommonJS you can delete the entry from require.cache:

JS
delete require.cache[require.resolve('./config')]
const fresh = require('./config')   // re-evaluated from disk
Cache-busting is a last resort
Deleting cache entries can produce duplicate, divergent copies of a module and is a frequent source of subtle bugs. Prefer designing modules so you do not need to reload them. **ES Modules have no equivalent** — the ESM cache is not user-mutable; tests use loader hooks or a fresh worker/process instead.
Key takeaways
  • A module evaluates once; later imports get the cached exports.

  • The cache key is the absolute resolved path — beware duplicate package copies.

  • Module scope = a free, explicit singleton for shared resources.

  • Modules that throw are evicted and will re-run; keep top-level code idempotent.

  • Cache-busting works in CJS via require.cache; ESM has no user-facing equivalent.

Next
Apply all of this by building your own reusable files in [Creating Local Modules](/nodejs/local-modules).