A production incident where React renders twice, hooks throw “Invalid hook call” errors, or a shared state store silently forks into two instances is almost always a symptom of the same root cause: singleton drift inside module federation. When you decompose a monolithic frontend into independently deployed remotes, the webpack runtime is responsible for negotiating a single shared dependency graph at load time. Get that negotiation wrong, and you end up shipping two copies of React, two Redux stores, or two instances of a design-system context provider into the same DOM tree. This article dissects the shared scope resolution algorithm inside module federation, the specific failure modes that cause singleton violations, and the production-grade configuration required to eliminate them at scale.
#The Bottleneck: Shared Scope Fragmentation
In a classic micro-frontend topology, a host application and N remotes are built independently, often by separate teams on separate release cadences. Each build declares its own shared block inside ModuleFederationPlugin. At runtime, webpack’s federation runtime constructs a shared scope — a global registry keyed by package name and version range — and each container attempts to register its own copy of a dependency into that scope before consuming it.
The bottleneck emerges because this negotiation is asynchronous and order-dependent. If Remote A initialises before the host has registered its React instance, and Remote A was compiled against a slightly different semver range, the federation runtime will silently permit a second React instance to load rather than fail loudly. This is by design — module federation defaults to graceful degradation over hard failure — but in a singleton-sensitive dependency like React, ReactDOM, or a context-based state manager, graceful degradation is precisely what causes runtime breakage.
The Internal Developer Platform – Part 2: Designing the IDP Control Plane with CRDs
#Architectural Breakdown of Module Federation Shared Scopes
To fix the problem you need to understand the three-stage resolution pipeline module federation executes for every shared module request.
#Stage 1: Scope Initialisation
On first container load, webpack creates a namespaced object on window.__webpack_share_scopes__ (or an equivalent isolated scope if shareScope is customised). Every remote and the host push their declared shared dependencies into this object, tagged with version, a get() factory function, and metadata flags including singleton, eager, and strictVersion.
#Stage 2: Version Range Matching
When a consumer requests a shared module, module federation runs a semver satisfaction check against every entry already registered in the scope. If multiple versions satisfy the requested range, the runtime by default selects the highest matching version — not the first-registered one. This is a critical detail most teams miss: load order does not determine the winner; semver range intersection does.
#Stage 3: Singleton Enforcement
If singleton: true is set and two incompatible versions are registered (i.e. their ranges do not intersect), the runtime either logs a console warning and loads both (default), or throws if strictVersion: true is also set. This is the lever that converts a silent architectural failure into a build-time or runtime-observable one, and it is the lever most teams leave disabled in production.

For teams standardising these decisions across multiple squads, embedding this logic into your organisation’s architectural patterns documentation prevents each micro-frontend team from independently reinventing shared scope policy.
#Implementation Logic: Enforcing Deterministic Resolution
The remediation strategy has four steps, applied consistently across host and every remote:
- Pin exact versions for singleton-sensitive packages rather than caret ranges, eliminating ambiguous semver intersections.
- Enable
strictVersionfor anything where a version mismatch causes runtime corruption rather than a cosmetic issue. - Introduce a build-time manifest diff check in CI that fails the pipeline before deployment, rather than relying on the runtime warning.
- Instrument the federation runtime with a custom
resolveSharehook to log and alert on any resolution event where more than one version was registered, even if satisfaction succeeded.
#Code and Configuration
The following ModuleFederationPlugin configuration reflects a locked-down shared scope for a host application. Note the explicit requiredVersion and strictVersion pairing — this is what converts module federation’s default “best effort” behaviour into a hard contract.
1// webpack.config.js (host)
2const { ModuleFederationPlugin } = require('webpack').container;
3
4module.exports = {
5 plugins: [
6 new ModuleFederationPlugin({
7 name: 'host',
8 remotes: {
9 checkout: 'checkout@https://cdn.kbytech.io/checkout/remoteEntry.js',
10 catalogue: 'catalogue@https://cdn.kbytech.io/catalogue/remoteEntry.js',
11 },
12 shared: {
13 react: {
14 singleton: true,
15 strictVersion: true,
16 requiredVersion: '18.2.0',
17 eager: true,
18 },
19 'react-dom': {
20 singleton: true,
21 strictVersion: true,
22 requiredVersion: '18.2.0',
23 eager: true,
24 },
25 '@kbytech/design-system': {
26 singleton: true,
27 strictVersion: false,
28 requiredVersion: '^4.3.0',
29 },
30 },
31 }),
32 ],
33};Every remote must mirror the singleton and strictVersion flags for React and ReactDOM exactly. A mismatch on these two flags between host and remote is the single most common cause of the “two React instances” bug in module federation deployments — the runtime treats singleton negotiation per-package, not globally, so one remote forgetting strictVersion: true silently reopens the door to drift.
To catch this before it ships, hook into the federation runtime’s share resolution lifecycle. Since webpack 5.75+, the @module-federation/runtime package exposes plugin hooks that let you intercept resolution events:
1// federation-runtime-plugin.js
2import { registerPlugins } from '@module-federation/runtime';
3
4registerPlugins([
5 {
6 name: 'singleton-drift-monitor',
7 beforeRequest(args) {
8 return args;
9 },
10 resolveShare(args) {
11 const { shareScopeMap, scope, pkgName, version } = args;
12 const registered = Object.keys(
13 shareScopeMap[scope]?.[pkgName] || {}
14 );
15 if (registered.length > 1) {
16 console.error(
17 `[module-federation] singleton drift detected for ${pkgName}: ` +
18 `${registered.join(', ')} — requested ${version}`
19 );
20 // Optionally forward to an APM/observability pipeline
21 window.__KBY_TELEMETRY__?.emit('mf_singleton_drift', {
22 pkgName,
23 versions: registered,
24 });
25 }
26 return args;
27 },
28 },
29]);Finally, gate deployments with a CI check that inspects each remote’s compiled manifest (mf-manifest.json, or the legacy remoteEntry.js metadata) and diffs shared dependency versions against a centrally maintained lockfile before allowing promotion to production:
1#!/usr/bin/env bash
2# ci/check-mf-drift.sh
3set -euo pipefail
4
5EXPECTED_REACT="18.2.0"
6MANIFEST_URL="$1"
7
8ACTUAL_REACT=$(curl -s "$MANIFEST_URL" | jq -r '.shared[] | select(.name=="react") | .version')
9
10if [ "$ACTUAL_REACT" != "$EXPECTED_REACT" ]; then
11 echo "FAIL: react version drift detected. Expected $EXPECTED_REACT, got $ACTUAL_REACT"
12 exit 1
13fi
14
15echo "OK: shared scope aligned for $MANIFEST_URL"#Failure Modes and Edge Cases in Module Federation
Even with strict versioning enforced, module federation exposes several edge cases that teams routinely underestimate:

Eager loading collisions. Setting eager: true on a shared dependency forces it to be bundled directly into the container’s initial chunk rather than resolved asynchronously from the shared scope. If both host and a remote mark the same package as eager, you get two independently instantiated copies registered into the scope simultaneously, and the “highest version wins” rule can select the remote’s bundled copy over the host’s, inverting your intended dependency ownership.
Deferred remote initialisation. Remotes loaded lazily via dynamic import() after the initial paint can register into the shared scope after singleton-dependent code has already executed against the host’s copy. In React specifically, this manifests as a context provider mismatch where a lazily loaded remote’s components subscribe to a different React instance’s context, silently returning default values instead of throwing.
Micro-frontend independent deployability vs. lockstep versioning. The entire value proposition of module federation is independent deployability. Strict version pinning across every remote reintroduces a lockstep release train, which is the exact coupling micro-frontends were meant to remove. Teams must consciously decide, per-dependency, whether independence or runtime safety takes priority — there is no configuration that gives you both simultaneously for a genuinely singleton-sensitive package.
CDN cache staleness. Since remotes are typically fetched from a CDN-hosted remoteEntry.js, an aggressive cache TTL on that manifest can mean production is running a stale shared scope declaration for minutes or hours after a deploy, even though the underlying remote bundle has updated. This produces intermittent, hard-to-reproduce singleton violations that only appear for a subset of users hitting stale edge nodes.
#Scaling and Security Trade-offs
Rolling this out across a large organisation with dozens of remotes requires explicit trade-off decisions, not a single “correct” configuration. The relevant considerations for architects standardising module federation at scale:
- Strict versioning vs. deployment velocity —
strictVersion: trueeliminates silent drift but forces every remote team to coordinate upgrades of shared singleton packages, effectively reinstating a release train for that dependency subset. - Runtime monitoring overhead — the custom
resolveSharehook adds negligible CPU cost (sub-millisecond per resolution) but requires a dedicated observability pipeline to be actionable; without alerting wired up, the console warnings are simply lost in production. - CDN TTL vs. manifest freshness — a short TTL (under 60 seconds) on
remoteEntry.jsreduces stale shared-scope risk but increases origin load and latency on every host bootstrap; a long TTL improves performance but widens the blast radius of a bad deploy. - Security surface of dynamic remotes — because module federation loads remote code at runtime from a URL, a compromised CDN or DNS hijack on a remote’s origin constitutes a direct code-execution vector into the host application; enforcing Subresource Integrity or signed manifest verification is non-negotiable for any remote sourced from a third party or an external network boundary, and should be layered on top of standard webpack module federation configuration rather than assumed as a built-in guarantee.
- Shared scope isolation vs. bundle size — using a custom
shareScopeper business domain (rather than one global default scope) prevents cross-domain singleton collisions entirely, at the cost of duplicating shared dependencies across scopes and increasing total transferred bytes.
None of these trade-offs are eliminated by tooling alone. The organisations that run module federation successfully at scale treat shared scope policy as a governed architectural contract, enforced in CI, rather than a per-team convention left to individual webpack configs.






