Dec 26, 2025·7 min read

Replace shared mutable globals with explicit state safely

Learn how to replace shared mutable globals by finding hidden singletons and moving state into request-scoped dependencies to prevent race conditions.

Replace shared mutable globals with explicit state safely

Why shared mutable globals cause weird, random bugs

“Shared mutable” sounds fancy, but it’s simple: a value lives in one place (shared), and your code can change it (mutable). When many parts of an app read and write that same value, behavior starts depending on timing, not intent.

That’s why these bugs feel random. With one user clicking around, the app might look fine. Under parallel requests, background jobs, or automatic retries, two bits of work can touch the same global at the same time. One request updates the value, and another request accidentally uses it, even though they’re different users or different tasks.

A common example is a currentUser variable stored in a module, a “global cache” that also stores per-request data, or a singleton client that quietly keeps state (headers, tokens, a selected tenant). These are exactly the cases where you want to replace shared mutable globals with explicit state.

Typical symptoms look like this:

  • Users see someone else’s data or get logged in as the wrong person
  • “Works on my machine” failures that only show up under load
  • Flaky tests that pass or fail depending on order
  • Random 401/403 errors because the wrong token was reused
  • Background jobs that pick up the wrong configuration

The goal isn’t “no shared things.” Databases and connection pools are shared on purpose. The goal is: shared resources stay shared, while request-specific state is passed in (or created per request) so nothing is silently reused.

This shows up a lot in AI-generated prototypes. FixMyMess often runs into hidden singletons that look harmless until real traffic hits.

What counts as a global or singleton in real projects

A “global” is any piece of state that lives outside a specific request, job, or user action, but still gets read and written while the app runs. A “singleton” is the same idea with a nicer label: “only one instance,” shared by everyone.

In real code, these aren’t always obvious. They hide in module-level variables, framework “services,” static class fields, or helpers that quietly store things in memory. If you’re hunting for shared mutable globals, this is the shape of what you’re looking for.

Not everything global is bad. Read-only constants are usually safe: version strings, fixed limits, and default settings. The risk starts when the value can change while requests are in flight. Mutable state plus concurrency creates the bugs that disappear when you add logs.

Shared state often shows up as:

  • Module-level caches or memoized values that store user-specific results
  • A shared config object that gets mutated (for example, “current tenant”)
  • A shared DB client wrapper that also stores per-request data like “current user”
  • Auth/session helpers that keep tokens in memory instead of per request
  • In-memory queues or “pending jobs” arrays used by multiple users

The danger rises as you scale. A single-user dev server may look fine, but multiple workers or instances make the behavior less predictable. Some failures only appear when two requests overlap.

If you inherited an AI-generated prototype, these “one instance” shortcuts are common. During an audit, FixMyMess often finds them around authentication, caching, and background work.

Quick signs you have hidden shared state

Hidden shared state usually shows up as problems that feel random. The app “mostly works,” then fails in ways you can’t reproduce on demand.

The clearest symptom is data bleeding between users. One request updates something, and the next request (from a different user or tenant) sees it. You might notice a user suddenly appears logged in as someone else, the wrong org name shows in the header, or a dashboard briefly loads another customer’s data.

Test behavior is another giveaway. A test passes when you run it alone, but fails when you run the full suite. That often means one test leaves behind state (like a cached currentUser, or a global database wrapper with mutable settings) that changes the next test.

Traffic makes it worse. When requests overlap during a spike, you get occasional 500 errors that vanish on retry. That pattern often points to shared objects being mutated mid-request, like a global config object, a singleton client with per-request headers, or a module-level “current request” variable.

A final tell: “It worked locally.” Many dev servers handle requests one at a time, so shared-state bugs stay hidden until production runs multiple requests at once.

Fast red flags to scan for:

  • A module-level variable that changes during a request (token, tenantId, currentUser)
  • A singleton client that stores per-request data (headers, auth, locale)
  • In-memory caches used as a source of truth (not just a speed-up)
  • Logs that show mixed request IDs or user IDs in the same flow
  • Bugs that disappear when you add print statements (timing changes)

Teams sometimes bring FixMyMess a prototype where two people log in at the same time and sessions cross. That’s almost always shared state, not “mysterious auth.”

How to track down hidden singletons step by step

Hidden singletons are a common reason you see “random” behavior under load: one request changes something, and the next request inherits it.

Work through your codebase in this order (it saves time and catches the biggest offenders first):

  • Scan for module-level variables that get reassigned (not just constants). Watch for names like currentUser, token, client, config, or session.
  • Hunt for singleton patterns: getInstance(), “only create once” comments, or lazy initialization like “if not created, create it now.”
  • Inspect caches and memoization. A cache can be fine, but it becomes dangerous if the key is missing, too broad (like just userId without tenantId), or defaults to one key for all requests.
  • Review request handlers and middleware for objects stored outside the handler. A common trap is creating an object once at startup, then mutating it per request.
  • Check framework “locals” and app-wide stores. Mixing up request locals and app locals turns per-request data into cross-request storage.

A quick reality check

To confirm you found the culprit: open two browser sessions (normal and incognito), log in as two different users, and hit the same endpoint a few times. If identities, permissions, or settings bleed across sessions, you almost certainly still have shared mutable state.

A simple model: request state vs shared resources

A reliable way to eliminate shared mutable globals is to split everything into two buckets: things that belong to one request, and things that are safe to share across requests.

Per-request state is anything that changes from user to user or call to call: the current user ID, auth claims, a correlation ID for logs, locale, and the specific input payload. This data should never live in a module-level variable, because two requests can overlap and overwrite each other.

Shared resources are expensive building blocks you can reuse safely: a database connection pool, an HTTP client with fixed settings, a compiled template. The key is that these objects must not store request-specific data inside them.

A simple rule: if it would be wrong for Request B to see it, it can’t be global.

A practical model that keeps you honest:

  • Put per-request state in function parameters or a small RequestContext object.
  • Build dependencies explicitly using constructors or a factory function.
  • Keep shared objects immutable (configuration) or internally safe (like a DB pool).
  • If something must be shared and mutable, protect it with proper synchronization.

Example: instead of a global currentUser, create ctx = { user, correlationId } when the request starts, then pass ctx into handlers and services. Your DB pool stays shared, but your query functions take ctx so logging and permissions stay correct.

Refactor plan: move from globals to explicit state

Stop cross-user data leaks
If users ever see the wrong data, we’ll pinpoint the cause before you commit.

To replace shared mutable globals safely, start small. Pick one risky area where the wrong state hurts fast, like auth, tenant selection, or caching. A focused change is easier to review and less likely to break unrelated features.

First, write down what the code is secretly pulling from “somewhere”: current user, tenant ID, feature flags, locale, request ID, and so on. Then create a tiny request context object that holds only what you truly need.

A sequence that works in most codebases:

  • Choose one entry point (an API route, handler, or job) and build the request context there.
  • Change one function at a time to accept the context as an argument instead of reading globals.
  • When a function needs a service (db, cache, auth client), pass it in or build it from a factory.
  • Keep shared resources shared (like a connection pool), but keep per-request data per-request.
  • After it works, remove the old global or make it fail loudly if accessed.

A factory can be the bridge that prevents a big rewrite. For example, createServices(ctx) can return authService, tenantService, and auditLogger that all read from the passed context, not from module-level variables. It also makes dependencies visible instead of implicit.

Finally, delete or freeze the old global. Don’t leave it around “just in case.” Someone will use it again during a quick fix.

Request-scoped dependencies without overengineering

The goal is straightforward: create what your code needs once per request, pass it in explicitly, and keep truly shared parts (like connection pools) read-only from the handler’s point of view. That’s usually enough to stop concurrency bugs without building a giant dependency system.

Keep the “request container” small. Treat it like a plain object that holds only what varies per request: the current user, request ID, locale, feature flags, and a clock. Everything else should be a shared resource that is safe to reuse.

A practical pattern in a web app:

  • App startup: create shared resources (DB pool, HTTP client, logger config)
  • Per request: create request state (user, request ID) and small helpers that need it
  • Handler: accept these dependencies as parameters, not via imports

For example, a login handler can build a RequestContext once, then pass it to services like AuthService(ctx, db_pool, logger). The DB pool is shared, but the context is not. That prevents one user’s data from leaking into another request when two requests run at the same time.

Shared dependencies that are usually safe include a DB connection pool (not a single connection stored globally), an HTTP client with no per-user headers baked in, and logger configuration (but not a mutable currentUser global).

Background jobs are where people slip back into globals because there is no request. Treat jobs the same way: create a JobContext with a job ID and any user/workspace ID the job belongs to, and pass it into the job function. If you only pass one value, pass the context.

Common mistakes that make the problem worse

Secure your AI-generated backend
We harden security issues often found in AI-generated code, including exposed secrets.

The fastest way to undo your own refactor is to keep the global and try to “reset it” on every request. That looks safe in single-user testing, but it breaks the moment two requests overlap. If one request resets the value while another is still using it, you get behavior that’s painful to reproduce.

Another classic trap is storing the “current user” in a global variable or a singleton service. It feels convenient because you can access it from anywhere, but it turns every request into a race. You see symptoms like users becoming each other, permissions flipping, or audit logs showing the wrong actor.

Global caches are also risky when they don’t include tenant or user keys. A cache that uses only a product ID (or worse, a single “latest” value) can leak data across accounts. The bug doesn’t look like a cache bug. It looks like “my app sometimes shows someone else’s data.”

Some mistakes start as performance hacks but create reliability problems too. Creating a new database connection per request instead of using a pool can exhaust connections under load. Then you “fix” it with retries and timeouts, which hides the real issue and makes failures harder to reason about.

Mixing mutable config with runtime state is another quiet killer. If you change a config object at runtime (feature flags, base URLs, environment values) and that object is shared, every request can see a different setup depending on timing.

Quick red flags that often show up together:

  • A singleton holds fields like currentUser, token, requestId, or lastResult
  • A cache key is missing tenantId or userId
  • “Reset” functions run in middleware or before handlers
  • DB connections are opened and closed in the request handler
  • Config objects are modified after startup

If you’re inheriting AI-generated code, these patterns show up a lot in prototypes. They often only surface once real users hit the app at the same time.

How to confirm the fix with simple tests

After you replace shared mutable globals, the code usually feels better right away. The real proof is that it stays consistent when two things happen at once.

1) Add one small concurrency test

You don’t need a huge test suite. Start with one test that fires two requests in parallel using different users (or different API keys) and asserts their responses never mix.

# Pseudocode example
# Send two parallel requests:
# - user A logs in and fetches /me
# - user B logs in and fetches /me
# Assert A never sees B's data, and B never sees A's data.

If this test fails even once, you still have shared state hiding somewhere.

2) Make cross-talk visible with logs

Add simple structured logs that include a request ID and user ID in every handler and in any service object you refactored. Then scan for impossible sequences, like user A’s request ID suddenly logging user B’s ID.

A few fast checks that surface hidden coupling:

  • Run the same test suite with parallel execution enabled.
  • Loop the concurrency test 50 to 200 times to catch nondeterministic failures.
  • Add an assertion that request-scoped objects are created per request (not reused).
  • Track memory during the loop to confirm it stays flat after removing accidental caches.
  • Temporarily lower timeouts to make race conditions show up sooner.

If you still see random failures, it usually means a leftover singleton (like a module-level client that stores “current user”) or a cache keyed too broadly.

Example: a prototype that breaks when two users log in

A common AI-generated prototype bug looks harmless in single-user testing: authentication state is stored in a global variable. For example, the app keeps currentUser (or an accessToken) in a module-level variable, and every API route reads from it.

Here’s what happens in production. User A logs in, then User B logs in a moment later. The global currentUser gets overwritten. Now, when User A clicks “My Account,” the server sometimes answers as User B. It feels random because it depends on timing, not on the code path.

Typical signs in logs and support tickets:

  • “I saw someone else’s data for a second”
  • Requests show the wrong user ID even though cookies look correct
  • The issue only appears under load or when two people test together
  • Refreshing sometimes “fixes” it

The fix is to stop asking a global singleton who the current user is. Instead, build an auth service per request, using explicit context (headers, cookies, session ID). Each request gets its own auth object, and handlers receive it as a parameter.

Concretely: parse the token from the incoming request, verify it, then pass the verified user to the functions that need it. Shared resources (like a DB connection pool) can stay shared, but user identity must be request-scoped.

After this refactor, concurrent logins and API calls become consistent: User A always sees User A’s data, even when User B is active.

Quick checklist before you ship

Make caching safe per user
We’ll audit caches and keys so user and tenant data can’t bleed across requests.

Before you ship, do one last pass to make sure you didn’t leave any shared state hiding in plain sight.

Ship-ready sanity checks

  • Scan request-handling code for module-level variables that change (anything written to during a request).
  • Double-check caches and memoization. Make sure keys include what separates users and tenants (and locale when it changes output).
  • Confirm request context is passed in, not read from hidden singletons. If a function needs the current user, auth token, tenant, or timezone, it should receive it as an argument (or through a request-scoped dependency).
  • Audit what you do share. Connection pools, immutable config, and read-only clients are usually fine. Anything with mutable fields (like currentUser, lastQuery, headers) is not.
  • Run a parallel test: two users logging in and doing different actions at the same time. Look for cross-user data, mixed sessions, or “random” permission errors.

If you inherited an AI-generated prototype, this checklist is worth doing twice. These apps often sneak in global “current user” state or single shared clients with mutable headers.

Next steps if your AI-generated code has concurrency bugs

If your app works fine with one user but fails under real traffic, treat it like a shared state problem until proven otherwise. Many AI-generated projects accidentally keep data in memory that should live inside a request, session, or database.

Start by making a quick inventory of anything that can be written to from more than one request. Look for module-level variables, cached objects that store user data, “singletons” created once on startup, and helper utilities that keep internal state.

A simple way to move forward is to fix one user journey end-to-end. Pick the path that breaks most often (login, checkout, file upload). Then refactor only that path so state is passed through function arguments or request-scoped dependencies. Once one path is clean, the patterns are easier and safer to repeat.

When you’re not sure where the hidden singleton is, narrow the search:

  • Add logs for object IDs and user IDs at key steps (auth, DB access, caching)
  • Grep for “global”, “singleton”, “cache”, “memo”, “static”, and module-level assignments
  • Temporarily disable in-memory caching to see if the bug disappears

Sometimes it’s faster to get an expert review, especially when the code mixes frameworks, background jobs, and custom auth. FixMyMess (fixmymess.ai) diagnoses and repairs AI-generated code, starting with a free code audit. Most projects are completed within 48-72 hours with AI-assisted tooling and expert human verification, so the fixes hold up under load.

FAQ

What exactly is a “shared mutable global”?

A shared mutable global is any value that lives outside a specific request or job and can be changed while the app is running. It becomes risky when multiple requests can read and write it, because one user’s work can overwrite another user’s state.

Why do shared globals cause bugs that feel random?

Because the outcome depends on timing, not just code flow. Under parallel requests, retries, or background work, two operations can overlap and accidentally reuse the same state, so the bug appears and disappears depending on load and scheduling.

What are common real-world examples of hidden globals or singletons?

Watch for things like a module-level currentUser, a singleton client that stores headers or tokens, a “global cache” that holds per-user results, or a shared config object that gets mutated (like “current tenant”). These patterns often work in single-user testing and fail when requests overlap.

What are the clearest signs that state is leaking between requests?

If users ever see the wrong account, the wrong tenant name, or data from another user, treat it as shared state until proven otherwise. Flaky tests that depend on order and occasional auth failures like random 401/403s are also strong signals.

What’s a quick way to reproduce or confirm the problem?

Open two sessions (for example, a normal window and a private window), log in as two different users, and hit the same endpoints repeatedly. If identity, permissions, or settings ever cross over, you still have request-specific data living in shared memory somewhere.

What can be shared safely, and what should never be shared?

Shared resources are fine when they don’t contain per-request data. A database connection pool, an HTTP client with fixed settings, and immutable configuration are typically safe; the danger starts when the shared object stores mutable fields like currentUser, token, tenantId, or per-request headers.

How do I replace a global `currentUser` without rewriting everything?

Create a small request context at the entry point (handler or middleware) containing only what changes per request, like user identity and a request ID. Then pass that context (or derived services created from it) into functions instead of importing a global that silently carries state.

How can I keep caching without leaking data across users?

Caching is fine when it’s a performance layer, not a hidden source of truth. The key is correct scoping and keys: include tenant and user when results differ per tenant/user, and avoid a single “latest” value that any request can overwrite.

How should I handle background jobs if there’s no “request”?

Treat a job like a request: create a JobContext with the job ID and any workspace/user identifiers the job belongs to, and pass it into the job function. Avoid reading or writing module-level state inside job runners, because multiple jobs can run at the same time.

What should I do if I inherited an AI-generated prototype that breaks under load?

Start with a focused audit around auth, caching, and any singleton clients, because those are frequent failure points in AI-generated prototypes. If you want a faster path, FixMyMess can run a free code audit to identify the hidden shared state and then repair and harden the code, with most projects finished in 48–72 hours.