
Building React 19 Micro Frontends on AWS CloudFront

Stewart Moreland
Executive summary
Micro frontends (MFEs) are best treated as independently deployable UI "services" that align to bounded contexts (domain-oriented slices), with a strong bias toward sharing as little business logic and state as possible across boundaries [1]. The most operationally sustainable approach on AWS is typically:
- Client-side composition (a shell loads MFEs at runtime), where each MFE ships static assets via S3 + CloudFront, and a "discovery" mechanism (manifest/import map/module federation remote registry) controls what versions are loaded [1].
- Use SSR selectively for the parts of the product that truly need it (SEO-critical, fast Time-to-First-Byte requirements, link-preview correctness, auth-gated but indexable pages, etc.), because SSR introduces a larger operational surface area (compute, cold starts, cache key hygiene, and rollback complexity). The AWS micro-frontend guidance explicitly distinguishes client-side vs server-side composition and highlights that server-side composition requires deeper expertise in deployment/discovery/cache management [1].
For CloudFront edge programmability, a practical default is:
- Prefer CloudFront Functions for URL rewrites/redirects, simple auth token checks, cache-key normalization, and header manipulation—these are explicitly called out as the "ideal" fit and run at very high scale with sub-millisecond execution; they do not have network access and can't read request bodies.
- Use Lambda@Edge when you need origin-request/origin-response hooks, network access, filesystem access, request body access, or longer-running code (e.g., SSR, complex personalization, image optimization).
- Keep SSR compute at the origin (CloudFront + ALB/ECS/Lambda URL) when you want simpler debugging and more predictable runtime characteristics, then use CloudFront caching + Origin Shield strategically.
React 19 Impact
React 19 materially affects architecture choices because it expands the "server-first" design space (Server Components + Server Functions) and improves streaming server rendering primitives (e.g., Web Streams rendering). However, some capabilities (notably "use client" / "use server" directives) are bundler/framework-mediated, so exact behavior is "implementation-defined" by your bundler or full-stack framework [2].
Architecture decisions for practical React 19 micro frontends
A rigorous micro frontend design starts with decisions that constrain the rest of the system. AWS Prescriptive Guidance frames micro frontends as distributed frontend services and discusses two broad setups: frontend-only MFEs (shared API layer) and full-stack MFEs (each with its own backend) [1].
Boundaries and ownership
The most durable boundary rule is: a micro frontend owns its UI, data, session/state, business logic, and flow inside a bounded context; it should "share as little business logic and data with other micro-frontends as possible," and any necessary sharing should happen through clearly defined interfaces (events, reactive streams) [1].
Intentional sharing is still appropriate for cross-cutting concerns like a design system or logging libraries, but treat these as platform-owned products with explicit versioning and compatibility guarantees.
Composition options and what they imply
You typically choose one of these composition styles:
- Client-side composition: the browser loads the shell, then MFEs (scripts/CSS/HTML fragments) at runtime. AWS describes a common pattern where a shell discovers MFEs via manifests, then loads micro-frontend bundles hosted in S3 and served by CloudFront; teams deploy new versions independently and update manifest info via pipelines they own [1].
- Server-side composition: an origin "UI composer" assembles HTML from multiple fragments, then CloudFront caches the result. AWS shows an example with CloudFront as a single entry point, a static S3 origin plus a UI composer origin (e.g., ALB + containers), and separately deployed micro-frontend fragments/services.
- Edge-side composition (ESI/SSI): AWS notes this is usually not chosen for new applications; it can be a migration pathway for legacy systems that already use transclusion.
In practice, mixed composition is common: a server-rendered shell establishes SEO and global chrome, and some MFEs are client-composed islands.
React 19 server/edge rendering primitives: what's stable vs unspecified
React 19 and the React 19.x line add/solidify:
- React Server Components (RSC): render "ahead of time" in a separate server environment; they can run at build time in CI or per request on a web server [2].
- Server Functions (formerly "Server Actions" terminology): used with Server Components; when passed to a form
action, React can progressively enhance forms so they can submit even before the JS bundle loads. - Streaming server rendering APIs:
renderToReadableStreamrenders HTML to a Web ReadableStream, which is a good fit for edge-style runtimes that speak the Fetch/Web Streams model.
Bundler-Dependent Behavior
The "use client" / "use server" directives are described as bundler features that create "split points" between environments. That means a "from-scratch" micro frontend stack (without a full-stack framework) must treat directive semantics as bundler-dependent (unspecified unless your toolchain documents it) [2].
Rendering models and trade-offs for SSR, SSG, and SPA micro frontends
This section treats SSR/SSG/SPA as deployment-time and runtime contracts, not as ideological choices. The "right" mix is usually per-route, per-MFE, and per-user journey.
Comparative table
| Dimension | SSR micro frontends | SSG micro frontends | SPA micro frontends |
|---|---|---|---|
| Operational complexity | Highest (compute + caching + rollbacks across dynamic HTML) | Medium (build-time complexity, simpler runtime) | Lowest (static hosting + API layer) |
| Latency to first byte | Potentially best when cached at edge; worst when uncached/cold | Excellent (static HTML from CDN) | Often slower first interaction (JS boot), but fast on repeat visits |
| SEO & link previews | Strong (HTML is present immediately) | Strong (HTML is present immediately) | Weak unless you add pre-rendering or SSR |
| Cacheability at CloudFront | Harder: must design cache keys carefully | Excellent: immutable static assets + cacheable HTML | Excellent for assets; HTML fallback patterns required |
| Personalization | Powerful but risky (cache fragmentation, auth coupling) | Limited (must defer to client/API) | Strong on client post-auth |
| MFE independence | Good, but tighter coupling to compositor/runtime contracts | Good (independent builds) | Excellent (independent builds) |
| Edge compute fit | Lambda@Edge or origin SSR; Functions only for glue | Mostly Functions for rewrites + headers | Functions for routing/rewrites + headers |
(These are implementation-level tendencies; real-world results depend on cache policy, bundle size, and how many MFEs are co-rendered per route.)
SSR variant: recommended practice for React 19 MFEs on CloudFront
When SSR is justified SSR is most justified when you need HTML on first response (SEO, crawlability, link previews, or critical performance journeys). It is also relevant if you are adopting React Server Components and want to execute server-only data fetching/rendering per request [2].
React 19-specific SSR mechanics
If your runtime supports Web Streams, renderToReadableStream is a canonical rendering primitive. This aligns conceptually with "edge runtimes," but whether you can use it directly in Lambda@Edge depends on your runtime's stream support and your request/response integration layer (treat as toolchain-specific).
CloudFront placement options for SSR
- CloudFront + origin SSR: CloudFront forwards SSR-worthy routes to an origin (ALB/ECS, Lambda function URL, etc.) and caches responses where safe. CloudFront caching reduces origin load and reduces latency by serving more objects from edge locations.
- Lambda@Edge SSR: Use Lambda@Edge for SSR or other "computationally intensive origin request and response operations" when you need origin hooks, network, bigger packages, or request-body access. AWS positions Lambda@Edge as an extension of Lambda that runs code closer to viewers; you author functions in a single region (US East - N. Virginia) and CloudFront runs them at edge locations.
Cache-key discipline for SSR CloudFront cache policy and origin request policy work together: cache policy defines what goes into the cache key; origin request policy defines what is forwarded even if not part of cache key. As soon as SSR output varies per user (cookies, auth headers, locale headers), you risk cache fragmentation. A practical approach is to:
- SSR only what is stable across many users (or cache per segment/locale),
- push genuinely user-specific data to the client (or a BFF) after load [3],
- and keep your cache key minimal (forward only what you truly need).
SSG variant: recommended practice for React 19 MFEs on CloudFront
SSG is usually the "sweet spot" for MFEs that are mostly content-driven, marketing pages, docs, and product browse pages where data doesn't need to be fresh per request.
React Server Components can run once at build time (CI) which conceptually aligns well with SSG workflows [2]. In micro frontend terms, SSG is operationally attractive because:
- each MFE can produce static artifacts independently,
- CloudFront can cache them aggressively,
- rollbacks are often just "switch manifest pointer back."
SPA variant: recommended practice for React 19 MFEs on CloudFront
SPAs remain operationally simple: host and cache static assets, call APIs for data. AWS's guidance explicitly notes client-side-rendered micro-frontends consuming centralized APIs and optionally using a BFF in the bounded context to reduce chattiness [3].
The "gotcha" on CloudFront is deep-link routing: /app/orders/123 must return index.html (or the MFE HTML entry) rather than 404. CloudFront supports custom error responses (e.g., mapping 404/403 to a custom page) that can return different response codes.
In practice, SPA routing is often implemented with either:
- CloudFront custom error response mapping 403/404 →
/index.html(return 200), or - CloudFront Functions viewer-request rewrite (preferred when you need selective behavior per path). CloudFront Functions are designed for URL rewrites/redirects and header manipulation at high scale.
Composition techniques and cross-microfrontend contracts
Module Federation as the default MFE "runtime composition" technique
Webpack Module Federation's core model is that each build is a container that can expose modules and consume modules from other containers, and that shared modules can be deduplicated across builds.
The ModuleFederationPlugin supports strict version checks (e.g., strictVersion) that can reject a shared module at runtime if the required version is not found—important when you want a hard guarantee about React singletons.
A practical baseline for React MFEs is:
- Shell = host container
- MFEs = remotes exposing route-level components or "mount" functions
- Shared deps =
react,react-dom, router, design system primitives - Business code = not shared (avoid leaking domain across boundaries)
Example (Webpack host app)
// webpack.config.js (host)const { ModuleFederationPlugin } = require('webpack').containermodule.exports = {plugins: [new ModuleFederationPlugin({name: 'shell',remotes: {catalog: 'catalog@/mfe-catalog/remoteEntry.js',checkout: 'checkout@/mfe-checkout/remoteEntry.js',},shared: {react: { singleton: true, requiredVersion: '^19.0.0' },'react-dom': { singleton: true, requiredVersion: '^19.0.0' },},}),],}
(Exact remote URLs are usually generated from a manifest or environment mapping to avoid hard-coding; see "Routing + manifests" below.)
Alternatives to Module Federation
Import maps Import maps are now part of the HTML standard; they let you map module specifiers to URLs and must be processed before modules that rely on them. In micro frontend systems, import maps are attractive because they make "which version is live" a data/config problem (change the map) rather than a code problem. AWS Prescriptive Guidance explicitly calls out import maps (and SystemJS) as a way to specify where modules load from at runtime [1].
single-spa orchestration single-spa positions itself as a framework for bringing together multiple JavaScript microfrontends; it's commonly used for route-based orchestration. single-spa also explicitly notes that it complements Module Federation: single-spa structures routes, Module Federation is a performance technique for runtime dependency sharing.
Web Components / Custom Elements as integration boundary
Custom elements let you register new HTML tags backed by a class extending HTMLElement.
For communication, DOM CustomEvent is a standardized way to attach custom data to events.
AWS's micro-frontend guidance explicitly mentions native DOM events (CustomEvents) as a messaging mechanism for cross-MFE communication [4].
Routing strategies: browser routing + CloudFront routing must agree
AWS explicitly notes that routing options depend on composition: with client-side composition you can use server-side routing (MPA) or client-side routing (SPA); edge-side and server-side composition align better with server-side routing, potentially augmented by edge compute such as Lambda@Edge [1].
A practical, implementation-focused routing taxonomy:
Path-based routing (single domain, one CloudFront distribution)
/→ shell/mfe-catalog/*→ catalog origin (or same origin prefix), SPA fallback to/mfe-catalog/index.html/mfe-checkout/*→ checkout origin This matches CloudFront's native model: each cache behavior maps to exactly one origin (or one origin group). Be careful with cache behavior order: CloudFront warns that misordered patterns can accidentally allow access that should have required signed URLs/cookies.
Subdomain routing (multiple CloudFront distributions)
app.example.com= shellcatalog.example.com= catalog MFEcheckout.example.com= checkout MFE This increases isolation, but cross-domain cookies/CORS become more complex and you'll often need a coordinated security header and auth strategy.
Hybrid: one domain for document routes, subdomains for assets This can reduce cache invalidations and keep routing simple, at the cost of more moving parts.
State management across microfrontends
AWS guidance is blunt: to reduce coupling, avoid a global state management accessible from all MFEs in the view (e.g., a shared Redux store), because it increases coupling. Instead, encapsulate state within MFEs and communicate asynchronously (messages/events) [4].
A pragmatic "state-sharing ladder" (use the lowest rung that works):
- URL as state (search params, path params): best for navigational context and shareable state.
- Events for domain signals: publish "userLoggedIn", "cartUpdated", "localeChanged" as semantic events.
- Thin shared session token: share only what is necessary (e.g., session ID or access token) via cookie or storage; each MFE fetches its own data (possibly via its BFF) [3]. AWS notes server-side sessions as an option where each MFE fetches required data via a session identifier, while keeping micro-frontend session data separate.
- Shared reactive stream/event bus library (platform-owned): AWS describes an approach where an event bus library is orchestrated by the shell and used by multiple MFEs; events are documented and agreed as contracts [4].
- Shared store: last resort; revisit boundaries if you find you need many shared state slices [1].
Communication patterns
- DOM events:
window.dispatchEvent(new CustomEvent("mfe:cartUpdated", { detail: {...} }))(works across frameworks). The CustomEvent API is standardized and widely supported. - Pub/Sub: a shell-owned event bus library can provide topic naming, payload validation, and observability hooks; AWS recommends asynchronous messaging for reducing coupling [4].
- Custom elements: strong encapsulation boundary, especially when MFEs are heterogeneous (React + other frameworks).
CloudFront deployment patterns and edge rendering options
This section is intentionally concrete: it focuses on CloudFront distribution design, caching, edge compute, and "gotchas" that sabotage MFE independence.
Distribution topology: single vs multiple distributions
One distribution (path-based behaviors) is operationally simpler for:
- consistent security headers,
- consistent auth cookies,
- simplified DNS and TLS,
- fewer moving parts for observability.
CloudFront supports multiple origins, but the API model is explicit: you need enough cache behaviors to actually use all origins, and each cache behavior specifies a single origin.
Multiple distributions (per MFE) can be valuable if:
- teams truly need independent CDN configuration change cadence,
- you need separate WAF policies,
- or you need strict blast-radius isolation.
In practice, many organizations do one distribution for the shell + shared assets, and allow some MFEs to own their own distribution if justified.
S3 origins: use OAC, avoid S3 website endpoints for private origins
If you want private S3-backed hosting via CloudFront, use Origin Access Control (OAC). AWS introduced OAC to secure S3 origins by permitting access to designated distributions only, using SigV4 signing to S3 [5].
CloudFront's "restrict access to S3 origin" guidance states that the origin must be a regular S3 bucket (not a website endpoint) and notes that OAC (and OAI) are not supported with S3 website endpoints. The S3 website hosting docs also recommend using OAC (vs OAI) when creating a CloudFront distribution to serve a static website securely [5].
CloudFormation snippet (OAC) (adapted from AWS documentation)
Resources:SiteOAC:Type: AWS::CloudFront::OriginAccessControlProperties:OriginAccessControlConfig:Name: my-site-oacOriginAccessControlOriginType: s3SigningBehavior: alwaysSigningProtocol: sigv4
Cache behaviors for microfrontends: concrete patterns
CloudFront cache behaviors let you attach different caching, headers, and edge compute policies per URL path pattern; the default behavior is the last processed.
A typical MFE behavior set (single distribution):
Default (*)→ shell origin (SSR origin or S3 origin depending on your shell variant)/assets/*→ static assets origin (S3), long TTL/mfe-catalog/*→ catalog origin (S3), SPA fallback rewrite to/mfe-catalog/index.html/mfe-checkout/*→ checkout origin (S3), SPA fallback rewrite/api/*→ API origin (ALB/API Gateway), caching disabled or carefully keyed
SPA routing rewrites with CloudFront Functions
CloudFront Functions are designed for URL rewrites and header manipulation at high scale; they run on viewer request/response events and are sub-millisecond.
A selective rewrite is often cleaner than global 404→index error mapping, because you can:
- rewrite only "document" requests (no file extension),
- keep true 404s for missing assets,
- support multiple MFEs under different base paths.
Example CloudFront Function (viewer request) for SPA routing
function handler(event) {var request = event.requestvar uri = request.uri// Only rewrite under a specific MFE base pathif (uri.startsWith('/mfe-catalog/')) {// If the request looks like a file (has a dot), don't rewriteif (uri.match(/\.[a-zA-Z0-9]+$/)) {return request}// Route all other paths to the MFE HTML entryrequest.uri = '/mfe-catalog/index.html'return request}return request}
SSR on the edge: Lambda@Edge vs "SSR at origin"
CloudFront's own guidance distinguishes the two edge compute families:
- CloudFront Functions: JavaScript (ECMAScript 5.1), viewer request/response only, no network, no filesystem, no request body.
- Lambda@Edge: Node.js/Python, viewer + origin request/response triggers, supports network and filesystem access and request bodies.
This makes the decision straightforward:
- If your SSR needs data fetching, you generally need Lambda@Edge or origin SSR.
- If you want to keep SSR logic "normal" (framework server, containers, easier local parity), put SSR at origin behind CloudFront.
Caching and invalidations: prefer versioning over invalidation
Versioning Over Invalidation
CloudFront explicitly recommends choosing between invalidations and versioned file names, and recommends primarily using file versioning if you update frequently. Invalidations have subtle behavior: when you invalidate a file, CloudFront invalidates every cached version regardless of cookies/header variants, so invalidations can be "broader" than expected in dynamic setups.
Cost and timing considerations:
- The first 1,000 invalidation paths/month are free; after that, you're charged per invalidation path.
- Invalidations are not immediate; AWS documentation notes they can take several minutes to propagate.
Practical cache-control pattern (implementation guidance)
- Hash/version static assets (
app.3f2c1.js) and cache them "forever" (long TTL). - Keep
index.html(and any MFE manifest/remoteEntry pointer) with a short TTL, or handle via controlled invalidation. This aligns with CloudFront's recommendation to use versioned file naming to control served versions.
Response headers, CSP, and CORS at CloudFront
CloudFront supports response headers policies to add/remove headers on responses it sends to viewers, without changing your origin. This is usually the simplest way to enforce:
- Security headers (HSTS, X-Frame-Options, etc.)
- CORS headers
- A baseline CSP (then tighten iteratively per MFE)
AWS also provides a CloudFront Functions example that sets Content-Security-Policy and other headers on viewer response events.
AWS recently reinforced that CloudFront can implement headers such as CSP and X-Frame-Options to mitigate common web vulnerabilities.
Authentication patterns at the edge and CDN layer
Signed URLs / signed cookies CloudFront supports restricting access to private content using signed URLs or signed cookies; AWS recommends also requiring access through CloudFront rather than direct origin URLs to prevent bypassing restrictions [5]. This is particularly relevant for:
- private micro frontend bundles (rare, but sometimes required),
- premium assets,
- multi-tenant back-office apps.
Lightweight request authorization in CloudFront Functions AWS notes CloudFront Functions are suitable for request authorization by validating hashed tokens such as JWTs by inspecting authorization headers or request metadata. This can work for coarse gating (e.g., "must have a valid token format"), but for full auth you typically still need an origin check (introspection, session validation), which pushes you toward Lambda@Edge or origin auth middleware.
Observability: logs and metrics that matter for microfrontends
CloudFront provides:
- Standard logs (access logs): detailed per-request information (time, paths, response codes, etc.).
- Standard logging (v2) can deliver logs to CloudWatch Logs, Firehose, or S3, and includes configuration steps and permissions.
- Real-time logs: configured with log fields and sampling rate delivered to Kinesis Data Streams.
- CloudWatch metrics: CloudFront publishes operational metrics for distributions and edge functions (CloudFront Functions and Lambda@Edge).
For MFEs, the practical observability requirement is correlation:
- propagate a request ID from CloudFront to your origin and to browser telemetry,
- ensure each MFE logs include MFE name + version (from manifest),
- and capture cache hit ratio and origin latency signals.
Rollback strategies: configuration, artifacts, and origin resilience
Artifact rollback (static assets) If you deploy immutable, versioned artifacts, rollback is usually a manifest pointer change (or import map change) rather than a destructive operation.
CloudFront continuous deployment (safe CDN config changes) CloudFront continuous deployment uses a staging distribution and a continuous deployment policy to route a portion of production traffic to staging [6]. Important constraints for planning:
- Traffic routing can be weight-based or header-based.
- Weight-based traffic shifting has a bounded range (the API reference indicates 0–0.15).
- CloudFront may route all requests to the primary distribution during peak service traffic conditions, and continuous deployment has compatibility constraints (e.g., HTTP/3 limitations are documented).
In micro frontend deployments, continuous deployment is particularly useful when:
- you must change cache behaviors/security headers at the distribution level,
- or you are migrating origins (e.g., switching SSR origin infrastructure).
Origin failover (availability, not feature rollback) Origin failover lets you create an origin group with a primary and secondary origin; CloudFront automatically fails over under configured HTTP failure responses. This is not a substitute for version rollback, but it is a strong resilience layer for SSR origins and critical asset origins.
Delivery pipelines, testing strategy, and performance optimization
CI/CD pipeline shape for microfrontends on CloudFront
AWS's micro-frontend reference architecture for client-side composition explicitly assumes that teams deploy new versions independently and update manifest information via pipelines they own [1].
A robust pipeline design typically separates:
- Build artifact creation (per MFE)
- Artifact publication (S3 upload with cache-control)
- Manifest update (pointer to new versions)
- Optional invalidation (only for non-versioned entrypoints)
Mermaid deployment flow (static MFEs + manifest)
Testing strategy: keep independence without breaking the whole
Micro frontend testing must span both local unit correctness and cross-MFE integration contracts:
- Unit + component tests per MFE: fast, owned by the team.
- Contract tests for integration surfaces: event schemas, exposed federation modules, shared library APIs.
- End-to-end tests: run on the composed system, validating critical user journeys across MFEs.
- SSR/edge tests: if using Lambda@Edge or origin SSR, test cache-key correctness and verify that personalized responses are not cached incorrectly (or that cache varies correctly).
Performance optimization: what matters most on CloudFront
Reduce origin load, maximize cache hit ratio CloudFront's caching guidance centers on increasing the proportion of requests served from edge caches (cache hit ratio) to reduce load and latency.
Use edge compute for "glue," not for heavy computation (unless needed) CloudFront Functions are positioned as ideal for high-scale, latency-sensitive customizations (rewrites, auth checks, header manipulations). Lambda@Edge is for heavier or origin-related workloads.
Origin Shield when cache misses are expensive Origin Shield is designed to improve cache hit ratio and reduce origin load by adding an additional caching layer.
React 19 performance levers
React Compiler is a build-time tool that automatically optimizes memoization patterns, reducing the need for manual useMemo / useCallback in many cases [2].
In MFE systems, this is especially relevant because MFEs often accumulate "defensive memoization" to reduce cross-tree re-renders; compiler adoption can simplify code while improving predictability (subject to compiler rollout constraints).
Cost and latency considerations (edge compute and invalidations)
For edge choices, the cost model often decides the architecture:
- CloudFront Functions pricing has historically been publicized as $0.10 per million invocations.
- Lambda@Edge pricing (from the Lambda pricing page) includes $0.60 per 1 million requests plus compute charges.
- Invalidation is free for the first 1,000 paths per month; above that you pay per path, and CloudFront invalidation requests are not instantaneous.
The architectural implication is consistent:
- Use CloudFront Functions for rewrites and routing (cheap, extremely low latency).
- Use Lambda@Edge only when you need its capabilities (origin triggers, network, request body).
- Minimize invalidations by using versioned assets and controlling TTL of a small set of entrypoints.
Architecture diagrams for SSR/SSG/SPA micro frontends on CloudFront
Reference architecture: single CloudFront distribution with mixed origins
This diagram is consistent with AWS guidance that micro-frontend bundles can be hosted in S3 and served through CloudFront, and that server-side composition can involve CloudFront with a static origin plus a UI composer origin [1].
When to pick each rendering model per route
A robust practical guide uses a per-route rubric:
- Prefer SSG for pages whose HTML can be generated at build time (marketing, docs, stable catalogs). React Server Components can run at build time as part of CI to support pre-rendering patterns [2].
- Use SSR where you need correct HTML per request. Use CloudFront caching, origin request policies, and possibly Origin Shield to keep origin load manageable.
- Use SPA where interactivity dominates and SEO isn't primary, but implement robust routing rewrites at CloudFront.