Back to Blog
Next.js Development

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

Dharmendra
Dharmendra
9 min read
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 bounds due 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:

  1. Next.js spawns multiple Node.js worker processes (typically matching CPU cores)
  2. Each worker processes a subset of your routes independently
  3. 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
  • globalThis doesn'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

  1. Create a new Next.js project:
npx create-next-app@latest singleton-test --typescript --app
cd singleton-test
  1. 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 };
  1. 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>;
}
  1. 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 CaseSymptomImpact
Prisma ClientToo many database connectionsConnection pool exhaustion
Shiki HighlighterMemory access out of boundsBuild crashes
Custom LoggersDuplicate log entriesDebugging confusion
Feature FlagsInconsistent flag valuesBuild-time race conditions
Configuration LoadersMultiple config readsPerformance 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

  1. Shared worker memory for module-level state during builds
  2. Build-time singleton registry that persists across workers
  3. 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

Subscribe to these issues for updates:

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 globalThis checks don't work across worker boundaries
  • Use React.cache for 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

  1. Audit your codebase for singleton patterns that assume single instantiation
  2. Implement the React.cache pattern for request-scoped data memoization
  3. Subscribe to GitHub issue #65350 for official updates
  4. Test your build pipeline with the reproduction steps above to verify your fixes work
  5. 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

#Tutorial#Advanced
Share:
Dharmendra

Dharmendra

Content creator and developer at UICraft Marketplace, sharing insights and tutorials on modern web development.

Premium Templates

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