Implementing Cloudflare Workers for Edge MFE Orchestration to Reduce TTFB
Honestly, when I first heard about micro-frontends (MFE), my immediate thought was, "Independent deployments per team? Sounds great — but doesn't that mean the user's browser has to download every team's bundle separately?" Anyone who has experienced the chronic pain of client-side composition — slow initial rendering as multiple MFE JS bundles load sequentially — will relate. Server-side composition is a bit better, but then the central server becomes a bottleneck and the core MFE value of independent deployment gets blurry.
Edge MFE runtime orchestration is an approach that overcomes both of these limitations simultaneously. It assembles each team's HTML and JS fragments at CDN edge nodes closest to the user and delivers them as a single response. Real-world references report noticeably reduced TTFB compared to central-server-based approaches (results vary by conditions and infrastructure configuration). This article explores why this paradigm is attracting attention now, how to actually configure a Router Worker with Cloudflare Workers, and the structure where ESI and a Manifest Resolver form a single system. It will be especially useful for those currently running client-side MFEs and looking to improve TTFB.
Core Concepts
Three Composition Approaches — Why Edge Is Different
How you compose MFEs broadly divides into three layers. The difference becomes clear when you look at where each approach handles assembly.
| Approach | Assembly Location | Characteristics | Main Drawbacks |
|---|---|---|---|
| Client-side composition | Browser | Maximum flexibility | Slow initial load, bundle bloat |
| Server-side composition | Central server | SEO-friendly | Central server bottleneck, difficult independent deployment |
| Edge-side composition | CDN edge node | Minimized TTFB, maintains independent deployment | Operational complexity, state management difficulty |
The key to edge composition: Edge nodes within 50ms of the user collect each team's MFE fragments and assemble them into a single integrated response. Without routing through a central server, round-trip latency is reduced, and each team still maintains independent deployment.
Core Components of Orchestration
To understand edge MFE orchestration, five concepts are worth knowing.
Router Worker is the gateway that routes incoming requests to the appropriate MFE Worker based on URL path. Service Binding is a Cloudflare-specific feature that enables direct communication between Workers within the same data center without HTTP round-trips. The key point isn't simply "it doesn't go through the public internet" — it's that there's no separate network round-trip at all, so latency approaches nearly zero compared to regular HTTP calls.
Remote Entry / Manifest is a spec that dynamically declares MFE bundle locations at runtime. ESI (Edge Side Include) is a markup technology that dynamically composes content at the CDN level using <esi:include> tags. Import Map may be unfamiliar — for backend developers, think of it as similar to the imports field in package.json. It's a browser-native module path declaration that maps statements like import 'team-a/App.js' to actual CDN URLs when the browser encounters them.
Module Federation 3.0 and Native ESM Federation
I used to think Module Federation was a Webpack-bound technology, but as of 2026, the story has changed considerably. Native ESM Federation federates MFEs using only browser-native import(), Import Maps, and Top-Level Await — without any Webpack build process.
<!-- Native ESM Federation using Import Maps -->
<script type="importmap">
{
"imports": {
"shell/": "https://cdn.example.com/shell/v2.1.4-a3f8c9d/",
"team-a/": "https://cdn.example.com/team-a/v1.8.2-b7e2f1a/",
"team-b/": "https://cdn.example.com/team-b/v3.0.1-c9d4e8b/"
}
}
</script>
<script type="module">
const { default: TeamAApp } = await import('team-a/App.js');
const { default: TeamBApp } = await import('team-b/App.js');
TeamAApp.mount('#section-a');
TeamBApp.mount('#section-b');
</script>The reason version hashes are included in URLs is CDN cache busting. The path must change with each deployment so the CDN reliably serves the new version.
One thing to watch out for is Import Maps' browser support status. Natively supported in Safari 16.4 and above and Chrome 89 and above — if you need to support older versions, polyfills like es-module-shims are required. Module version conflicts — for example, when team-a and team-b each require different versions of React — can be addressed by enforcing a single version at the Import Map level, or by isolating each MFE into a fully isolated Shadow DOM context.
Version Management via Manifest Endpoint
In the past, remoteEntry.js was referenced statically, but now the approach of dynamically resolving versions at runtime via a Manifest Endpoint is becoming widespread.
// GET /manifest.json — handled by an edge Worker
{
"version": "2.4.1",
"entries": {
"team-a": {
"url": "https://cdn.example.com/team-a/v1.8.2-b7e2f1a/remoteEntry.js",
"integrity": "sha384-abc123...",
"canary": false
},
"team-b": {
"url": "https://cdn.example.com/team-b/v3.0.1-canary.3-c9d4e8b/remoteEntry.js",
"integrity": "sha384-def456...",
"canary": true,
"canaryPercentage": 10
}
}
}This structure makes it natural to handle canary deployments, A/B testing, and gradual rollouts at the edge layer. From a deployment perspective, it's quite convenient — you can control traffic by changing only the Manifest without any code changes.
Practical Implementation
Now that we understand the concepts, let's look at how to actually implement this. The three examples below are not independent code snippets — they are layers that form a single system.
The Router Worker receives all requests and routes them to team-specific Workers. Some of those team Workers return HTML responses containing ESI markup. Which version of the client bundle to load from that HTML is determined by the Manifest Resolver. The three components connect in sequence to create the flow: "request routing → server fragment assembly → client bundle version resolution."
Example 1: Configuring Path-Based Routing with Router Worker
Since Cloudflare's official VMFE template was released, implementation patterns have become much clearer. The structure has the Router Worker look at the URL path and directly call each team's Worker via Service Binding. To be production-ready, fallback handling when a team Worker doesn't respond and request header forwarding must not be omitted.
// router-worker/src/index.ts
export interface Env {
TEAM_A_WORKER: Fetcher; // Service Binding: /products path
TEAM_B_WORKER: Fetcher; // Service Binding: /orders path
SHELL_WORKER: Fetcher; // Service Binding: shared layout
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Forward original headers so team Workers can use auth and region info
const forwardRequest = new Request(request);
try {
if (url.pathname.startsWith('/products')) {
return await withTimeout(env.TEAM_A_WORKER.fetch(forwardRequest), 3000);
}
if (url.pathname.startsWith('/orders')) {
return await withTimeout(env.TEAM_B_WORKER.fetch(forwardRequest), 3000);
}
return await env.SHELL_WORKER.fetch(forwardRequest);
} catch (err) {
// Explicit 503 response on team Worker failure (prevents infinite waiting)
const message = err instanceof Error ? err.message : 'upstream error';
return new Response(`Service unavailable: ${message}`, { status: 503 });
}
},
};
function withTimeout(promise: Promise<Response>, ms: number): Promise<Response> {
return Promise.race([
promise,
new Promise<Response>((_, reject) =>
setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
),
]);
}# wrangler.toml — Service Binding declaration
name = "router-worker"
[[services]]
binding = "TEAM_A_WORKER"
service = "team-a-worker"
[[services]]
binding = "TEAM_B_WORKER"
service = "team-b-worker"
[[services]]
binding = "SHELL_WORKER"
service = "shell-worker"| Code Point | Description |
|---|---|
Fetcher type |
Service Binding interface. Zero-latency communication within the same data center, no HTTP round-trip |
withTimeout |
Prevents the entire router from stalling when a team Worker is unresponsive |
new Request(request) |
Forwards original headers (cookies, auth tokens, CF-IPCountry, etc.) to downstream Workers as-is |
[[services]] |
Declares each team Worker as an independent service for separate deployment |
Example 2: Mixing Legacy Systems and MFE with ESI
If you're already using a CDN that supports ESI — such as Akamai, Cloudflare, or Fastly — you can introduce MFEs incrementally without replacing the legacy system all at once. This is the scenario encountered most often in practice. For Cloudflare, to process ESI in Workers response bodies, you need to disable the cf: { minify: { html: false } } setting and attach Surrogate-Control: ESI/1.0 to the response headers.
<!-- ESI markup processed at the CDN layer -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<esi:include src="https://legacy.example.com/fragments/header" />
</head>
<body>
<!--
onerror="continue": if this fragment request fails, the rest of the page renders normally.
ESI requests are processed in parallel by the CDN, so there's no sequential delay.
Note: timeouts must be adjusted separately in CDN vendor settings (Cloudflare default is 30s).
-->
<esi:include src="https://mfe-team-a.example.com/fragment/product-list"
onerror="continue" />
<div id="legacy-content">
<!-- Content rendered from the existing server -->
</div>
<esi:include src="https://mfe-team-b.example.com/fragment/cart"
onerror="continue" />
</body>
</html>// team-a/src/fragment.ts — fragment endpoint independently operated by Team A
export default {
async fetch(request: Request): Promise<Response> {
const html = await renderProductList();
return new Response(html, {
headers: {
'Content-Type': 'text/html',
// Cache for 1 minute, then refresh smoothly via stale-while-revalidate
'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
// Cache for 10 minutes at the CDN (Surrogate) layer
'Surrogate-Control': 'max-age=600',
},
});
},
};| Code Point | Description |
|---|---|
onerror="continue" |
Isolates a specific fragment failure so it doesn't block the entire page from rendering |
| ESI parallel processing | The CDN issues <esi:include> requests in parallel — not a sequential chain |
stale-while-revalidate |
Returns the previous version immediately after cache expiry, while refreshing in the background |
| Independent endpoint | Each team only manages their own Fragment URL, enabling independent deployment |
Example 3: Controlling Canary Deployments with Manifest Resolver
An edge Worker can dynamically decide which MFE version to deliver based on the characteristics of the requester. Where this example connects to the Router Worker → ESI fragment flow is that, after the client receives the page, it additionally requests /manifest.json to determine which JS bundle to load.
The charCode summation approach used in earlier drafts has uneven distribution, causing the actual canary percentage to differ. Using the FNV-1a hash below always produces the same result for the same userId while achieving a much more uniform distribution across 100 buckets.
// manifest-resolver/src/index.ts
export default {
async fetch(request: Request): Promise<Response> {
const userId = request.headers.get('X-User-Id') ?? '';
const isCanaryUser = shouldReceiveCanary(userId, 10); // 10% canary
const manifest = {
'team-a': isCanaryUser
? 'https://cdn.example.com/team-a/v1.9.0-canary.1-d3a8f2c/remoteEntry.js'
: 'https://cdn.example.com/team-a/v1.8.2-b7e2f1a/remoteEntry.js',
'team-b': 'https://cdn.example.com/team-b/v3.0.1-c9d4e8b/remoteEntry.js',
};
return Response.json(manifest, {
headers: { 'Cache-Control': 'no-store' }, // personalized responses must not be cached
});
},
};
function shouldReceiveCanary(userId: string, percentage: number): boolean {
// FNV-1a 32-bit: more uniform bucket distribution than simple charCode summation
let hash = 2166136261;
for (let i = 0; i < userId.length; i++) {
hash ^= userId.charCodeAt(i);
hash = Math.imul(hash, 16777619) >>> 0;
}
return (hash % 100) < percentage;
}| Code Point | Description |
|---|---|
| FNV-1a hash | Same userId → always the same bucket. The version doesn't change every time the user refreshes |
Math.imul(...) >>> 0 |
Safely handles 32-bit integer arithmetic in JS |
no-store |
Personalized responses must not be cached by the CDN. Only the Manifest is no-store; bundles use a separate caching strategy |
Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Ultra-low latency | Runs at the edge closest to the user. TTFB significantly improved by eliminating central server round-trips |
| Independent deployment | Full freedom of per-team CI/CD pipelines and technology stack choices |
| High availability | Eliminates single points of failure due to the distributed nature of edge nodes |
| Minimized cold starts | V8 Isolate-based architecture for millisecond-level cold starts |
| Flexible caching | Independent caching strategies per fragment when dynamic and static content coexist |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Operational complexity | Management burden of multiple edge Workers and routing rules | Automate Worker declarations with IaC such as Terraform, Pulumi |
| CDN dependency | Limited CDN support for ESI (Akamai, Cloudflare, Fastly, Varnish) | Minimize CDN dependency with Import Maps-based Native Federation |
| Statelessness constraint | Edge functions don't maintain state, making session management complex | Use edge storage like Cloudflare KV, Durable Objects |
| Local development difficulty | Hard to reproduce the edge environment (Web API only) locally | Set up Miniflare or Wrangler local mode from the start |
| Vendor fragmentation | ESI behavior differs subtly across CDN vendors | Minimize dependency on vendor-specific features; use a common abstraction layer |
| Organizational maturity required | Without clear API contracts and inter-team governance, benefits are diminished | Document interface contracts; establish a shared design system first |
V8 Isolate: An isolated execution environment provided by the Chromium V8 engine. It can start in tens of milliseconds without containers, making it the core technology that solves the cold start problem for edge functions.
stale-while-revalidate: An HTTP caching directive. After the cache expires, it immediately returns the previous version while fetching the new version in the background. Users always receive a fast response.
The Most Common Mistakes in Practice
-
Mixing ESI and client-side composition without unifying caching policy. When our team first introduced ESI, we experienced a situation where different versions were stored in server, edge, and browser caches respectively — causing certain users to see the cart UI rendered with a different version than the header. Explicitly declaring both
Surrogate-ControlandCache-Controlwas the first item on our checklist. -
Introducing edge orchestration without inter-team API contracts. If you start without prior agreement on what interfaces each team's fragments should expose, the Router Worker quickly fills up with per-team exception handling. Defining fragment URL patterns, caching header specs, and error response formats in JSON Schema first makes everything much easier later.
-
Postponing local development environment setup. The edge environment has the constraint of only using Web APIs — Node.js APIs are unavailable — and discovering this right before deployment makes debugging extremely painful. Setting up
wrangler devor Miniflare at the start of the project prevents this problem proactively.
Closing Thoughts
Edge MFE runtime orchestration becomes the most powerful option when simultaneously pursuing both team independence and user experience. However, there's one thing to honestly ask yourself before adopting this technology. For internal tools with low traffic, or small products with only one or two development teams, the complexity of operating multiple edge Workers may cost more than the benefits it provides. "Are there actually multiple teams that need to deploy independently?" is the fastest criterion for judging whether this technology is the right fit.
Three steps you can start with right now:
-
You can clone Cloudflare's official VMFE template and run it locally. Starting with the command
npm create cloudflare@latest -- --template=cloudflare/workers-sdk/templates/worker-mfegenerates a Router Worker along with two sub-Workers, letting you immediately experience the Service Binding structure firsthand. -
It's recommended to identify one small area in your currently running monolith that can be deployed independently and separate it out as an ESI fragment. A good first candidate is a UI block owned by a different team or one with a noticeably different update frequency from the rest of the page. For example, if the header is frequently changed by the design team while the product listing is managed by the commerce team, separating just the header as an ESI fragment allows each to deploy independently. The key is finding an area where two sections with different change cycles are bundled into a single deployment unit.
-
It's worth documenting inter-team MFE interface contracts as a Manifest Endpoint spec. Defining in JSON Schema which paths return which fragments and what the caching policies are makes configuring the orchestration layer much smoother afterward.
References
- Micro-frontends 2026: Module Federation 3.0 & Native ESM Federation
- Cloudflare Launches Vertical Microfrontend Template for Path-Based Edge Routing | InfoQ
- Building vertical microfrontends on Cloudflare's platform
- Microfrontends · Cloudflare Workers docs
- Approaching Micro-frontend Orchestration | Mia-Platform
- Edge-Rendered Frontends with CDN Functions: Balancing Performance, Cost, and Complexity
- Micro Frontends Patterns#10: Edge Side Includes | DEV Community
- Micro Frontends Patterns#11: Edge Side Composition | DEV Community
- MFE Orchestrator - Microfrontend Versioning & Orchestration
- Module Federation Runtime Guide
- Composing pages and views with micro-frontends | AWS Prescriptive Guidance