next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Next App Router (14.2.3) - Inconsistent Singleton

Open vaneenige opened this issue 1 year ago • 6 comments

Link to the code that reproduces this issue

https://github.com/vaneenige/next-app-router-singleton

To Reproduce

  1. Install dependencies
  2. Run the build - npm run build
  3. See the log of Create random multiple times

Current vs. Expected behavior

Current behavior:

  • We are rendering 100 pages that all use a singleton module.
  • During the build it logs the initialization multiple times (~5)

Expected behavior:

  • It should run the initialization once
  • It's also odd that it doesn't run it 100 times, but rather just a few times.

In the reproduction repository you can find a few different test cases:

  • With a class that is initialized
  • With the globalThis (check and override)
  • With the React.cache (async) function

After quite a bit of research I found multiple sources claiming that this pattern does work with the pages directory, but also multiple sources that can't get it to work with the app directory.

  • Using the globalThis method in pages: https://github.com/vercel/next.js/discussions/15054#discussioncomment-658138
  • Multiple users having issues with it in app: https://github.com/vercel/next.js/discussions/48481#discussioncomment-5686272
  • Shiki (syntax highlighter) causing memory issues (the solution is in the reproduction, but doesn't behave as expected): https://github.com/shikijs/shiki/issues/567
  • Prisma suggesting the globalThis solution to prevent too many connections: https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices

Having a proper singleton could benefit a lot of use cases such as database connections or separate worker scripts.

I've seen the issue being discussed on Reddit and GitHub with regards to MongoDB (or other persistent API connections) too.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.0.0: Fri Sep 15 14:41:34 PDT 2023; root:xnu-10002.1.13~1/RELEASE_ARM64_T8103
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 18.17.1
  npm: 9.6.7
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 14.2.3 // Latest available version is detected (14.2.3).
  eslint-config-next: 14.2.3
  react: 18.3.1
  react-dom: 18.3.1
  typescript: 5.4.5
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Module Resolution

Which stage(s) are affected? (Select all that apply)

next build (local)

Additional context

No response

vaneenige avatar May 04 '24 10:05 vaneenige

Are you sure it's not becaues of the Webpack Build Worker? This can split your build over a few different workers. This means that webpack is run X times over your app and therefore you may see X amount of logs called.

You can opt out of it: https://nextjs.org/docs/messages/webpack-build-worker-opt-out#webpack-build-worker-opt-out although you will most likely see a degrade in build times.

bitttttten avatar May 04 '24 17:05 bitttttten

I thought this could be it (as I was already thinking it's running separate builds in parallel), but it doesn't seem to change anything. When I add the following it still runs the initialization multiple times (1 time for every ~20 pages):

const nextConfig = {
  experimental: {
    webpackBuildWorker: false
  }
}

vaneenige avatar May 05 '24 15:05 vaneenige

I have the same problem. I tried to use globalThis in dev mode as Prisma's suggestion, but it does not work.

hungcung2409 avatar Jun 21 '24 13:06 hungcung2409

Same here, doing exactly like this file: https://github.com/vercel/next.js/blob/canary/examples/with-mongodb/lib/mongodb.ts but it's recreated multiple times (twice at the start of the application, 1 other time when calling inside a server action from client, third party component).

This happens only on production build

ioulian avatar Jun 25 '24 19:06 ioulian

Same problem. I have the same observations using both a singleton and globalThis.

Singleton

// test.ts
class Singleton {
  static instance = { value: 0 }
}

export default Singleton.instance

globalThis

// test.ts
if (!globalThis.test) {
  globalThis.test = { value: 0 }
  console.log('INITIALIZED')
} else {
  console.log('ALREADY INITIALIZED')
}

export default globalThis.test

Server

I created a TRPC procedure to expose the value and increment it on each request.

import test from "./test"
...
query(() => {
    if (Utility.isNull(test)) {
      return { test: -1 }
    }

    test.value += 1

    return { test: test.value }
  })
...

Frontend

Add button to trigger query on click.

Display test.value to check that it increments correctly.

Observation

The value is correctly incremented

  • on each request
  • when I reload the page
  • when I run the same test in an incognito window

however

The logs for the globalThis version constantly show INITIALIZED, which means that an instance of test.ts is constantly being created on each request.

Conclusion

This is very confusing, because according to the logs, it's broken. But according to the QA tests, it works.

I'm guessing that even though singletons and using globalThis both work, the NextJS server isn't optimized for it and will still spend time and resources initializing it on every request, which can be a huge problem if some libraries require time-consuming and expensive initialization.

Is there an explanation for this scenario, or is it a black box?

cyril-marblism avatar Jul 01 '24 13:07 cyril-marblism

I have been researching this at some length, coming at this from a slightly different angle but I think the core issue is the same. The reason you see your initialization logic occurring more than once is due to Next internally using two different module systems, CJS for the server context, and webpack for the browser context. You can see in the Next server there are actually a number of different mechanisms for importing modules.

What was useful in my case to understand this was using console.trace to debug the module initialization, because you'll be able to see the initialization context this code is getting run in. Here is what the server CJS imports look like:

[build:node-service]     at Object.<anonymous> (.../packages/server-core/lib/test.js:15:9)
[build:node-service]     at Module._compile (node:internal/modules/cjs/loader:1369:14)
[build:node-service]     at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)
[build:node-service]     at Module.load (node:internal/modules/cjs/loader:1206:32)
[build:node-service]     at Module._load (node:internal/modules/cjs/loader:1022:12)
[build:node-service]     at Module.require (node:internal/modules/cjs/loader:1231:19)
[build:node-service]     at require (node:internal/modules/helpers:179:18)
[build:node-service]     at Object.<anonymous> (../server-core/lib/index.js:38:14)
[build:node-service]     at Module._compile (node:internal/modules/cjs/loader:1369:14)
[build:node-service]     at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)

And here is what the RSC imports look like:

[build:node-service]     at eval (webpack-internal:///(rsc)/../../packages/server-core/lib/test.js:18:9)
[build:node-service]     at (rsc)/../../packages/server-core/lib/test.js (../services/nextjs-demo-service/dist/server/app/page.js:602:1)
[build:node-service]     at __webpack_require__ (../services/nextjs-demo-service/dist/server/webpack-runtime.js:33:42)
[build:node-service]     at eval (webpack-internal:///(rsc)/../../packages/server-core/lib/index.js:50:14)
[build:node-service]     at (rsc)/../../packages/server-core/lib/index.js (../services/nextjs-demo-service/dist/server/app/page.js:426:1)
[build:node-service]     at __webpack_require__ (../services/nextjs-demo-service/dist/server/webpack-runtime.js:33:42)
[build:node-service]     at eval (webpack-internal:///(rsc)/./src/app/page.tsx:10:79)
[build:node-service]     at (rsc)/./src/app/page.tsx (../services/nextjs-demo-service/dist/server/app/page.js:651:1)
[build:node-service]     at Function.__webpack_require__ (../services/nextjs-demo-service/dist/server/webpack-runtime.js:33:42)

This is a problem for a lot of other libs which rely on singletons, or otherwise need to use the state of objects which are loaded into the RCS context. In my case this completely breaks prometheus metrics which are used in your component code. But this seems like a pretty fundamentally hard problem to overcome, I'm not sure how you'd avoid re-importing something that needs to exist in multiple module formats.

MichaelSitter avatar Jul 03 '24 22:07 MichaelSitter

@MichaelSitter thanks for digging into that. I've also notice the different module importing issue causing singletons not to work, now I know why. Is this intended or a bug/bad design?

Fwiw, using instrumentation.ts properly runs code there once.

khuezy avatar Jul 20 '24 23:07 khuezy

Also hitting this, I have a socket with a single connection, but it seems a client component that calls a server action and a server component that calls a server action create different bundles in development, thus globalThis is not shared?

wesbos avatar Aug 21 '24 21:08 wesbos

Hi everyone!—

I have discussed this with the :nextjs: team and will be sharing some thoughts on this here.

First, during build, by default we use multiple workers. These are distinct child processes so unique copies of node that each load the module system and build some subset of pages. So for each worker spawned during the build you will see a copy of singleton initialization. We don't optimize for singletons because it would force us to use only 1 cpu core (JS is single-threaded) when for many machines more are available and thus builds can be faster if we parallelize.

Second, cjs vs webpack module loading seems like a bug/misconfiguration. In the first trace the module is being loaded by node which means it was at some point treated as an external. The second trace shows the same module being loaded by webpack so it wasn't treated as an external. This might be because the module is being used in RSC and client and we intentionally fork these modules so you can have one implementation for RSC and one for the client. In the future we may actually render the RSC part in it's own process which again would force there to be two different module instances.

The pattern of using a singleton during build or render to have a global view of the state of the server is just something that we are currently not optimizing for. It is maybe possible to mark something as "must be external" which means that it would again be a single instance across RSC and client (on the server).

Let me know if that clarifies!

samcx avatar Aug 29 '24 19:08 samcx

Thanks for the info @samcx . The first makes sense and is expected, but the 2nd can be confusing and cause unintended bugs. Please let us know when the "must be external" flag is implemented.

khuezy avatar Aug 29 '24 19:08 khuezy

Thank you for bringing this up with the team, @samcx! I've faced this issue in plugin development where I need one instance of the TypeScript compiler and Shiki syntax highlighter which can be very expensive to run. For now, I've opted to have people run a separate process that I can control better to avoid this since it can cause OOM issues, but breaks down the DX. Since this problem seems very similar to the instrumentation hook, it would be great to expose that in the next.js configuration rather than a file so plugin authors could make use of this as well.

Something like the following in next config would be great if possible:

export default {
  instrumentation: ['init-singleton-here.ts']
}

souporserious avatar Aug 29 '24 20:08 souporserious

There issues seem to be related: #55885 #49309

VanCoding avatar Oct 03 '24 17:10 VanCoding

I have this in a /lib/runtime.ts file:

const globalForRuntime = global as unknown as { runtime: AppRuntime | undefined };

export const appRuntime =
	globalForRuntime.runtime || createAppRuntime();

if (process.env.NODE_ENV !== "production") globalForRuntime.runtime = appRuntime;

I see this createAppRuntime called a few times in production... and I assume this is one of the reasons why I have a too many clients connected issues with my postgres database.

In my dev environment I see this function called multiple times, and when I log global it gets constantly set to undefined!

How do others usually handle this?

canastro avatar Jan 07 '25 09:01 canastro

How do others usually handle this?

This is because multiple workers are created which can cause many other hard-to-debug issues. One way to handle this is to store the information in process.env, which is shared across multiple workers.

khuezy avatar Jan 07 '25 15:01 khuezy

One way to handle this is to store the information in process.env, which is shared across multiple workers.

Yeah, storing information in process.env seems to be the only reliable way right now. I don't believe it worked pre v15 iirc, but this is currently what I do in one of my projects that works:

import { createServer } from 'renoun/server'

if (process.env.RENOUN_SERVER_STARTED !== 'true') {
  createServer()
  process.env.RENOUN_SERVER_STARTED = 'true'
}

export default {
  // ...
}

You can have more granular control based on the phase if needed.

souporserious avatar Jan 07 '25 18:01 souporserious

It's 2025 now... And this problem seems not to be solved...

PrinOrange avatar Mar 11 '25 04:03 PrinOrange

Hello! This is a critical bug for my application aswell! Having multiple instances of a class that should only be one causes a lot of problems!

npyl avatar Mar 16 '25 11:03 npyl

Also had this issue using tsyringe DI Singletons. Impossible to use if I can't even make sure an instance of my server side rate limiter cannot be shared between different requests. I feel like there should be an easy way to work with this.

flawnn avatar Apr 17 '25 16:04 flawnn

In my case I am using Prisma + Turso and I accomplished this by initing my Prisma/Libsql clients from within instrumentation.ts. Both my Prisma and Libsql instances are set on globalThis.

prisma.ts:

export default {
  ...
  initClient: () => {
    globalThis.client = getPrismaClient()
  },
}

instrumentation.ts:

import prisma from './lib/prisma'

export function register() {
  prisma.initClient()
}

Seems to work fine.

rlfrahm avatar Apr 25 '25 21:04 rlfrahm

In my case I am using Prisma + Turso and I accomplished this by initing my Prisma/Libsql clients from within instrumentation.ts. Both my Prisma and Libsql instances are set on globalThis.

prisma.ts:

export default { ... initClient: () => { globalThis.client = getPrismaClient() }, } instrumentation.ts:

import prisma from './lib/prisma'

export function register() { prisma.initClient() } Seems to work fine.

I tried your approach, but together with a Tsyringe DI container, I just get weird errors that I can't get to get behind. Basically, whenever I try to reach a page, it just errors out, as the DI Container of the Worker (which processes the request) is not accessing the globalThis container (or maybe can't?) and can't access the entities I want to resolve.

Error as a reference:

 ⨯ Error: TypeInfo not known for "ExtractsService"
    at eval (src/server/trpc/routers/extracts.ts:15:45)
    at <unknown> (rsc)/./src/server/trpc/routers/extracts.ts (/Users/flawn/Documents/Projects/Work/aponeo-dashboard/.next/server/app/page.js:14082:1)
    at __webpack_require__ (.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(rsc)/./src/server/trpc/root.ts:7:75)
    at <unknown> (rsc)/./src/server/trpc/root.ts (/Users/flawn/Documents/Projects/Work/aponeo-dashboard/.next/server/app/page.js:14071:1)
    at __webpack_require__ (.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(rsc)/./src/trpc/server.ts:12:75)
    at <unknown> (rsc)/./src/trpc/server.ts (/Users/flawn/Documents/Projects/Work/aponeo-dashboard/.next/server/app/page.js:14230:1)
    at __webpack_require__ (.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(rsc)/./src/app/page.tsx:17:70)
    at <unknown> (rsc)/./src/app/page.tsx (/Users/flawn/Documents/Projects/Work/aponeo-dashboard/.next/server/app/page.js:234:1)
    at Function.__webpack_require__ (.next/server/webpack-runtime.js:33:43)
  13 |
  14 |
> 15 | const extractsService = globalThis.container.resolve(ExtractsService);
     |                                             ^
  16 | const handleExtractError = createTRPCErrorHandler("processing extract");
  17 |
  18 | export const extractsRouter = createTRPCRouter({ {

Setup:


declare global {
  // eslint-disable-next-line no-var
  var container: DependencyContainer;
}

// Function to configure the dependency injection container
export function configureContainer() {
[...]
  globalThis.container = container;
}

export { container };

flawnn avatar May 12 '25 20:05 flawnn

Hi everyone!—

I have discussed this with the :nextjs: team and will be sharing some thoughts on this here.

First, during build, by default we use multiple workers. These are distinct child processes so unique copies of node that each load the module system and build some subset of pages. So for each worker spawned during the build you will see a copy of singleton initialization. We don't optimize for singletons because it would force us to use only 1 cpu core (JS is single-threaded) when for many machines more are available and thus builds can be faster if we parallelize.

Second, cjs vs webpack module loading seems like a bug/misconfiguration. In the first trace the module is being loaded by node which means it was at some point treated as an external. The second trace shows the same module being loaded by webpack so it wasn't treated as an external. This might be because the module is being used in RSC and client and we intentionally fork these modules so you can have one implementation for RSC and one for the client. In the future we may actually render the RSC part in it's own process which again would force there to be two different module instances.

The pattern of using a singleton during build or render to have a global view of the state of the server is just something that we are currently not optimizing for. It is maybe possible to mark something as "must be external" which means that it would again be a single instance across RSC and client (on the server).

Let me know if that clarifies!

https://github.com/vercel/next.js/issues/49309#issuecomment-2235268065 https://github.com/vercel/next.js/issues/49309

@samcx Just want to refer to this related issue here as well - there should be a really better way to handle these things!

flawnn avatar May 12 '25 20:05 flawnn

Running into this when trying to setup a DI container. Agreed that this is confusing and seems to be adds odds with ECMAScript specifications.

moneydance avatar May 14 '25 20:05 moneydance

I've been searching and finally finding these bug reports, what a time suck this has been for me! My singletons for typeorm end up doubled, and if I try the global trick typeorm doesn't work.

This is the first project I've done trying to use node for backend work, and I was beginning to blame node. Singletons for services are a big deal for back ends, we are not all doing serverless calls. Having moderate caches for connections and small bits of data are key for saving traffic/resources, so I hope this gets resolved soon or I'll have to (gulp) bail on next which I love otherwise!

ricklabanca avatar May 20 '25 14:05 ricklabanca

I've been searching and finally finding these bug reports, what a time suck this has been for me! My singletons for typeorm end up doubled, and if I try the global trick typeorm doesn't work.

This is the first project I've done trying to use node for backend work, and I was beginning to blame node. Singletons for services are a big deal for back ends, we are not all doing serverless calls. Having moderate caches for connections and small bits of data are key for saving traffic/resources, so I hope this gets resolved soon or I'll have to (gulp) bail on next which I love otherwise!

I think you should opt for seperate backend & migrate as soon as possible and use Next.js as a BFF (Backend for Frontend). Seems to be the only viable option.

flawnn avatar May 20 '25 17:05 flawnn

Using globalThis works for me... is everyone who can't get this to work on the latest Nextjs version?

The caveat is that Nextjs (webpack) will bundle duplicate code into chunks, so your "singleton" file will get executed multiple times. But as long as your wrap your singletons in globalThis, it should work.

Note: This didn't work in the past because Nextjs would run different jest workers for different tasks, eg, pages/app router, API routes, middleware, etc... but now everything is in the same process so globalThis should share context.

khuezy avatar Aug 13 '25 21:08 khuezy

Maybe this is a solution from the official source code:

https://github.com/redis-developer/session-store-nextjs/blob/main/lib/redis-adapter.js

import { createClient } from "redis";
import crypto from "crypto";

let redisClient;

export async function getRedisClient() {
  if (!redisClient) {
    redisClient = createClient({
      url: process.env.REDIS_URL,
      password: process.env.REDIS_PASSWORD || undefined,
    });

    if (!redisClient.isOpen) {
      await redisClient.connect();
    }

    redisClient.on("error", (err) => {
      console.error("Redis Client Error", err);
    });
  }

  return redisClient;
}

// ...

And here is one more implementation from the official source:

https://github.com/vercel/storage/blob/main/packages/kv/src/index.ts

import { Redis } from '@upstash/redis';
import type { ScanCommandOptions, RedisConfigNodejs } from '@upstash/redis';

let _kv: Redis | null = null;

// ...

export const kv = new Proxy(
  {},
  {
    get(target, prop) {
      if (!_kv) {
        if (!process.env.KV_REST_API_URL || !process.env.KV_REST_API_TOKEN) {
          throw new Error(
            '@vercel/kv: Missing required environment variables KV_REST_API_URL and KV_REST_API_TOKEN',
          );
        }

        _kv = createClient({
          url: process.env.KV_REST_API_URL,
          token: process.env.KV_REST_API_TOKEN,
        });
      }

      // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- [@vercel/style-guide@5 migration]
      return Reflect.get(_kv, prop);
    },
  },
) as VercelKV;

Usage of the second FYI:

https://vercel.com/changelog/vercel-kv

import kv from '@vercel/kv';

export async function getPrefs() {
  const prefs = await kv.get('prefs');
  return prefs || {};
}

export async function updatePrefs(prefs: Record<string, string>) {
 return kv.set('prefs', prefs);
}

sundaycrafts avatar Sep 15 '25 00:09 sundaycrafts

No, that's the same problem as discussed above, webpack will still bundle that code into different chunks.

khuezy avatar Sep 15 '25 00:09 khuezy

I have been facing the same issue. To confirm:

  1. Next.js / webpack will not respect running a module once, thus breaking a core principle of how modules work in Node.js and browser runtimes, because it creates many copies, aka. chunks, of the same module? That seems unnecessary, not sure why one chunk per module is not enough. I might be missing something.

  2. Adding to globalThis will prevent the issue, as that variable is still global to the whole app despite chunking.

Is that right?

magnusriga avatar Sep 16 '25 14:09 magnusriga

@magnusriga yes, webpack doesn't respect it intentionally (rude!) because that's the point of chunking the code to optimize load speed. There are some options to mark a file as a singleton but I never got it to work.

khuezy avatar Sep 16 '25 15:09 khuezy

@magnusriga yes, webpack doesn't respect it intentionally (rude!) because that's the point of chunking the code to optimize load speed. There are some options to mark a file as a singleton but I never got it to work.

Haha, rude indeed.

Quite surprising, a lot of code relies on modules running once. It is a core principle, and promise of, the JS runtimes.

magnusriga avatar Sep 16 '25 16:09 magnusriga