Why Next.js App Router Singletons Are Inconsistent (And How to Fix It)

Why Next.js App Router Singletons Are Inconsistent (And How to Fix It)
If you've been working with Next.js App Router and suddenly found your carefully crafted singleton pattern initializing multiple times during builds, you're not alone. The Next App Router Inconsistent Singleton issue has become one of the most frustrating bugs affecting developers since version 14.2.3, with over 129 reports on GitHub linking to the core issue.
This deep-dive will dissect exactly why your singletons are misbehaving, how to reproduce the problem, and—most importantly—how to fix it with production-ready solutions.
The Problem: Understanding 'Inconsistent Singleton' in Next.js App Router
The singleton pattern is foundational in modern web development. Whether you're managing database connection pools with Prisma, initializing expensive syntax highlighters like Shiki, or maintaining application-wide state, you expect a singleton to initialize exactly once.
However, in Next.js App Router (v14.2.3+), developers are observing bizarre behavior:
- Multiple initializations during build: A singleton designed to run once executes 5-10 times during
next build - Inconsistent state across pages: Different pages in the same build receive different singleton instances
- Memory issues: Libraries like Shiki throw
RuntimeError: memory access out of boundsdue to multiple instantiations
Here's what a typical developer expects versus what actually happens:
// lib/singleton.ts - Expected: Logs once during build
class MySingleton {
private static instance: MySingleton;
private constructor() {
console.log('🔧 Singleton initialized'); // Should appear ONCE
}
static getInstance(): MySingleton {
if (!MySingleton.instance) {
MySingleton.instance = new MySingleton();
}
return MySingleton.instance;
}
}
export const singleton = MySingleton.getInstance();Expected output during build:
🔧 Singleton initialized
Actual output with 100 pages:
🔧 Singleton initialized
🔧 Singleton initialized
🔧 Singleton initialized
🔧 Singleton initialized
🔧 Singleton initialized
This Next App Router Inconsistent Singleton behavior breaks fundamental assumptions about module-level code execution.
Why This Happens: Deep Dive into Next.js 14.2.3+ App Router Instantiation Mechanics
To understand this issue, we need to examine how Next.js App Router handles module instantiation differently from the Pages Router.
The Worker Pool Architecture
Next.js uses parallel workers during builds to speed up static generation. When you run next build:
- Next.js spawns multiple Node.js worker processes (typically matching CPU cores)
- Each worker processes a subset of your routes independently
- Each worker has its own module cache—singletons are NOT shared across workers
This architecture explains the "magic number" of 5-10 initializations: it roughly corresponds to the number of worker processes, not the number of pages.
Module Resolution in React Server Components
React Server Components (RSCs) add another layer of complexity:
- Server Components run in a separate module graph from the bundler
- The module cache is request-scoped in development and worker-scoped in production builds
globalThisdoesn't behave as expected because each worker operates in isolation
The Fatal Flaw in Traditional Patterns
Traditional singleton patterns that work in Pages Router fail in App Router because:
// ❌ This pattern FAILS in App Router builds
let instance: Singleton | null = null;
export function getSingleton() {
if (!instance) {
instance = new Singleton(); // Creates new instance per worker!
}
return instance;
}Each worker starts fresh—there's no shared memory between them during the build process.
Reproducing the Bug: Step-by-Step Guide and Common Scenarios
Before implementing fixes, let's reliably reproduce the Next App Router Inconsistent Singleton issue. This reproduction helps validate your fixes work correctly.
Minimal Reproduction Setup
- Create a new Next.js project:
npx create-next-app@latest singleton-test --typescript --app
cd singleton-test- Create the singleton module:
// lib/singleton.ts
class TestSingleton {
public readonly id: string;
constructor() {
this.id = Math.random().toString(36).substring(7);
console.log(`[BUILD] Singleton created with ID: ${this.id}`);
}
}
// Traditional singleton pattern
const globalSingleton = new TestSingleton();
export { globalSingleton };- Generate multiple pages that use the singleton:
// app/page-[id]/page.tsx (create 50+ of these)
import { globalSingleton } from '@/lib/singleton';
export default function Page({ params }: { params: { id: string } }) {
return <div>Singleton ID: {globalSingleton.id}</div>;
}- Run the build and observe:
npm run build 2>&1 | grep "Singleton created"You'll see multiple creation logs, confirming the Next App Router Inconsistent Singleton behavior.
Common Scenarios Where This Manifests
| Use Case | Symptom | Impact |
|---|---|---|
| Prisma Client | Too many database connections | Connection pool exhaustion |
| Shiki Highlighter | Memory access out of bounds | Build crashes |
| Custom Loggers | Duplicate log entries | Debugging confusion |
| Feature Flags | Inconsistent flag values | Build-time race conditions |
| Configuration Loaders | Multiple config reads | Performance degradation |
Immediate Workarounds: Mitigating the Issue with Practical Strategies
While waiting for a permanent fix from the Next.js team, here are battle-tested workarounds ordered by reliability.
Strategy 1: React.cache for Request-Scoped Memoization
For data that needs to be consistent within a single request tree, use React's cache function:
// lib/singleton.ts
import { cache } from 'react';
class ExpensiveResource {
constructor() {
console.log('Resource initialized');
}
async getData() {
// Expensive operation
}
}
// Memoized per-request (NOT per-build)
export const getResource = cache(() => new ExpensiveResource());Limitation: This only works for request-scoped consistency, not build-time singleton behavior.
Strategy 2: globalThis with Symbol Keys (Partial Fix)
For development mode and runtime (not build-time), use Symbol keys on globalThis:
// lib/singleton.ts
const SINGLETON_KEY = Symbol.for('app.singleton');
interface GlobalWithSingleton {
[SINGLETON_KEY]?: MySingleton;
}
class MySingleton {
private static get instance(): MySingleton {
const globalObj = globalThis as GlobalWithSingleton;
if (!globalObj[SINGLETON_KEY]) {
globalObj[SINGLETON_KEY] = new MySingleton();
}
return globalObj[SINGLETON_KEY];
}
public static getInstance(): MySingleton {
return MySingleton.instance;
}
}
export const singleton = MySingleton.getInstance();Limitation: Workers during builds have separate globalThis contexts—this won't prevent multiple initializations during next build.
Strategy 3: Lazy Initialization with External State
For database connections specifically, defer initialization until runtime:
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const prismaClientSingleton = () => {
return new PrismaClient();
};
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma;
export { prisma };This is Prisma's officially recommended pattern and works well for development hot-reloading, though it doesn't fully solve build-time issues.
The Proper Solution & Best Practices: Designing for Singleton Consistency in RSC
Given the architectural constraints, here's how to design robust singleton patterns for Next.js App Router.
Accept Build-Time Multiplicity, Enforce Runtime Consistency
The key insight: build-time multiple instantiation is often acceptable if runtime behavior is correct.
// lib/config-singleton.ts
import { cache } from 'react';
interface AppConfig {
apiUrl: string;
features: Record<string, boolean>;
}
// This WILL run multiple times during build - that's OK
async function loadConfig(): Promise<AppConfig> {
// During build: runs per worker (acceptable)
// At runtime: runs once per request (correct)
const config = await fetch(process.env.CONFIG_URL!).then(r => r.json());
return config;
}
// Request-scoped memoization ensures consistency within a render tree
export const getConfig = cache(loadConfig);Idempotent Initialization Pattern
Design your singletons to be idempotent—safe to initialize multiple times:
// lib/idempotent-singleton.ts
class IdempotentSingleton {
private initialized = false;
async initialize() {
if (this.initialized) return; // Safe to call multiple times
// Perform one-time setup
await this.setupConnections();
this.initialized = true;
}
private async setupConnections() {
// Connection logic that handles already-exists gracefully
}
}
// Each worker gets its own instance, but behavior is consistent
export const appSingleton = new IdempotentSingleton();Separating Build-Time and Runtime Concerns
For the Next App Router Inconsistent Singleton issue specifically, consider separating concerns:
// lib/singleton.ts
const isBuildPhase = process.env.NEXT_PHASE === 'phase-production-build';
export function getSingleton() {
if (isBuildPhase) {
// Return a lightweight stub during builds
return createBuildStub();
}
// Full singleton logic for runtime
return getRuntimeSingleton();
}Future Outlook: Expected Fixes and Long-Term Stability in Next.js
The Next.js team is aware of this issue. Based on GitHub issue #65350 and related discussions:
What's Being Explored
- Shared worker memory for module-level state during builds
- Build-time singleton registry that persists across workers
- Improved documentation clarifying expected behavior in App Router
Version Timeline
- Next.js 14.x: Issue present, workarounds required
- Next.js 15+: Partial improvements in worker coordination
- Future versions: Full singleton consistency is on the roadmap but no confirmed release date
Recommended Monitoring
Subscribe to these issues for updates:
Related Issues & Community Discussions: Broader Context and Shared Experiences
The Next App Router Inconsistent Singleton problem connects to several broader ecosystem challenges:
Database Connection Management
The Prisma community has extensively documented workarounds. The globalThis pattern became standard specifically because of hot-reload causing connection pool exhaustion in development.
Syntax Highlighting Libraries
Shiki maintainers created specific documentation for Next.js integration, recommending lazy initialization patterns.
The Pages Router Comparison
Notably, the Pages Router (/pages directory) doesn't exhibit this behavior because:
- It uses a single-process build model for static pages
- Module caching is shared across all route compilations
Developers migrating from Pages Router to App Router are particularly surprised by this behavioral change.
Community Solutions
Several community packages have emerged:
- next-global-state: Runtime state management designed for App Router
- singleton-manager: Worker-aware singleton patterns
However, evaluate these carefully—some add complexity without fully solving build-time issues.
Key Takeaways
- The root cause is architectural: Next.js App Router uses multiple workers during builds, each with isolated module caches—this is by design for performance
- Traditional singleton patterns fail: Class-based singletons and simple
globalThischecks don't work across worker boundaries - Use
React.cachefor request-scoped consistency: This is the officially recommended pattern for data that needs to be consistent within a request tree - Design for idempotency: Build singletons that are safe to initialize multiple times, with consistent behavior regardless of instantiation count
- Separate build-time and runtime logic: For truly critical singletons, detect the build phase and handle it differently
Next Steps
- Audit your codebase for singleton patterns that assume single instantiation
- Implement the React.cache pattern for request-scoped data memoization
- Subscribe to GitHub issue #65350 for official updates
- Test your build pipeline with the reproduction steps above to verify your fixes work
- Consider architectural alternatives like external state stores (Redis, databases) for truly global state
The Next.js App Router is powerful, but understanding its instantiation model is crucial for building robust applications. By designing with these constraints in mind, you'll avoid the frustration that has affected thousands of developers and build more resilient systems.
Tags
Dharmendra
Content creator and developer at UICraft Marketplace, sharing insights and tutorials on modern web development.
Build Your Next Project Faster
Save hours of development time with our premium Next.js templates. Built with Next.js 16, React 19, and Tailwind CSS 4.
Subscribe to our newsletter
Get the latest articles, tutorials, and product updates delivered to your inbox.
Related Articles

Turbopack serverExternalPackages Not Found with pnpm? Fix It
Turbopack can't locate packages in serverExternalPackages when they're transitive dependencies installed via pnpm. Learn why pnpm's strict isolation conflicts with Turbopack's resolution strategy and discover 4 proven fixes—from selective hoisting to direct installation.

Fix: Why 'use cache' Is Ignored in Next.js Dynamic Routes (and Correct Usage)
Confused why your 'use cache' directive isn't working in Next.js dynamic routes? This guide explains the key difference between development and production behavior, reveals the generateStaticParams requirement, and provides proven patterns for implementing caching correctly.

Why Next.js RSC Performance Suffers with CDNs in Highload Projects (And How to Fix It)
React Server Components (RSC) promise excellent performance, but pairing them with CDNs in highload scenarios introduces unexpected challenges. Learn why RSC payloads create unique caching friction points and how to optimize your architecture for global scale.