The Hardest Part of Leaving a CDN Isn't the CDN
A global equipment manufacturer wanted to consolidate its public website onto AWS and retire Akamai. Most of what Akamai did had a direct AWS equivalent: bot management, geolocation, thousands of redirect rules, edge caching. One piece did not. Every page on the site, across sixteen locales and roughly 710 templates published from OpenText TeamSite, was assembled at Akamai's edge using Edge Side Includes (ESI), and nothing else could render it.
Klish Group researched the options, built three working proofs of concept against the customer's real pages, and delivered the one that won: a native ESI resolver that now renders every regional site at the origin.
At a Glance
39 ESI Constructs In Use
12 directives, 14 expression functions and 13 variable types, and only one of them, esi:include, is part of the W3C ESI standard.
Three Working Prototypes
Apache Traffic Server, a native Rust resolver, and Fastly Compute, each built and run against the customer's real pages before anyone chose.
About 4 ms Per Page
Full page assembly, including a 124 KB header with the breadcrumb logic, compared with 27 ms for an optimized Python resolver.
No New Vendor
Built on an MIT-licensed open-source engine and run on infrastructure the customer already owns, with no platform contract and no per-request fee.
The Challenge: A Dialect, Not a Standard
TeamSite published finished HTML files to an Apache origin, and Apache served them untouched. The templates carried ESI markup that Akamai evaluated at the edge on its way to the visitor. Over the years the templates had come to depend on Akamai's proprietary extensions: variable assignment, loops with break, string and date functions, regular-expression matching, computed map lookups, and a custom directive that pulls JSON data into a page.
The site's breadcrumb shows how far that went. It was not stored anywhere. It was computed on every request, at the edge, from the visitor's URL, by around 150 lines of ESI that loop over path segments and check each one against a list of roughly 600 exclusion patterns and 200 label overrides.
Moving that logic into client-side JavaScript was ruled out early. These are SEO-critical product pages, and the breadcrumb feeds structured data, page speed scores and link previews on social media. The pages had to arrive finished.
Every Page Depends on It
The header and footer, which every page includes, both used Akamai-only constructs. Leaving even one page on standard ESI was not an option.
Includes Within Includes
Product and listing pages resolve data in chains: a product page loads an offers file, which points to offer folders, which supply detail-card fragments, each with its own ESI.
No Change for Authors
Content authors had to keep publishing from TeamSite exactly as they always had. Rewriting 710 templates was not an option either.
What We Evaluated
We started by measuring how much of the customer's ESI each off-the-shelf processor could actually handle.
| Processor | Coverage of the dialect | Verdict |
|---|---|---|
| Varnish (open source) | About 3%: include, remove, comment |
Ruled out |
| Varnish Enterprise | About 15-20%; adds conditionals and variables, but no loops or evaluation | Ruled out, and commercially licensed |
| NGINX SSI / nginx-esi | Less than Varnish | Ruled out |
| Apache Traffic Server ESI plugin | About half the directives, no expression functions, no loops | Best off-the-shelf option: prototyped |
Open-source fastly/esi engine |
Full dialect with targeted patches | Prototyped twice: on Fastly's edge and natively on the origin |
Option A: Apache Traffic Server
ATS assembles includes in front of Apache. Because its plugin covers only half the dialect, a custom Python pre-resolver evaluates everything else first. Two rendering components working together.
Option B: Native Rust on the Origin
One compiled resolver does all the ESI work on each web server: evaluates the dialect, fetches every include, and returns finished HTML. CloudFront is the only cache.
Option C: Fastly Compute
The same open-source engine compiled to WebAssembly and run on Fastly's edge (Fastly Compute), between CloudFront and the origin. Pages are assembled near the visitor.
What the Prototypes Revealed
Apache Traffic Server looked like the safe choice: a mature, open-source caching proxy with a real ESI plugin. Running the customer's actual templates through it told a different story.
- It fails silently. A single unsupported directive anywhere in a template returns
HTTP 200with an empty body. No error, no warning, no log line. - Error handling doesn't catch it. ESI's own
try/exceptfires only when an include fails to load, not when the parser rejects the template. - One bad fragment breaks the whole page. A clean page that includes a fragment with unsupported syntax is cut off at the include. Since every page includes the header and footer, the whole site was effectively unsupported.
- It was unstable under test. We reproduced a plugin crash and a cache-poisoning state that could only be cleared by rebuilding the container.
The pre-resolver worked: with it in place ATS rendered every page type correctly. But it meant writing a full ESI evaluator anyway and then running a second, more fragile engine behind it. Once a complete evaluator was needed regardless, the case for ATS came down to its in-region cache. With a single-region origin, that cache cannot hide cross-region latency on a miss. Only CloudFront can.
Fastly's open-source fastly/esi engine, patched for the Akamai dialect, handled all 12 directives and 14 functions. That left a question of where to run it: on Fastly's paid edge, or natively on the customer's own servers.
Side by Side
| A: Traffic Server | B: Native Rust | C: Fastly Compute | |
|---|---|---|---|
| Where pages are assembled | Origin | Origin | Fastly's global edge |
| Rendering components | Two (Python + ATS plugin) | One | One, on a vendor platform |
| Unsupported syntax | Silent empty page | Explicit, handled errors | Explicit, handled errors |
| Compute per cache miss | ~27 ms + ATS assembly | ~4 ms | Runs at the edge, billed per request and CPU time |
| Cache invalidation on publish | CloudFront plus a purge to every web server | CloudFront only | CloudFront plus Fastly |
| Vendor dependency | None | None | A second commercial CDN |
We recommended Option B. It removed the failure modes we had found, it ran faster than either alternative, and it didn't replace one CDN vendor with another, which was the whole point of the migration.
What We Built
We forked the MIT-licensed fastly/esi crate and removed its dependency on Fastly's platform SDK. Its parser, expression evaluator and function library were already separate from the platform code, so the correctness-critical logic was kept unchanged. On top of it we added patches for the Akamai dialect, support for the custom JSON directive, and a native orchestrator that fetches and inlines every include.
The result is a single static Linux binary that runs as a small service on each web server. Apache passes page requests to it over the local network interface; it reads the published templates from disk, resolves every directive and include, and hands back finished HTML with no ESI left in it.
| Resolver | Full page assembly |
|---|---|
| Python, first version | 190 ms |
| Python, optimized | 27 ms |
| Native Rust | ~4 ms |
Rolled Out One Site at a Time
A Switch Per Hostname
The web servers generate their Apache configuration from a plain-text site list. One flag on a hostname's line moves that site to the resolver, or moves it back.
Region by Region
Dedicated test hostnames first, then each regional site in turn over about eight weeks, until every site in every environment was served by the resolver.
Tuned From Live Traffic
The resolver spends most of its time waiting on includes, not computing, so we sized its worker pool for concurrency rather than CPU count, then adjusted it based on measured queueing and tail latency.
What We Took Away
- Measure before you choose. On paper, Apache Traffic Server was the obvious answer. Only running the real templates through it exposed the silent empty pages.
- Reuse the hard part. We didn't write an ESI parser from scratch. A well-maintained open-source engine supplied the correctness-critical logic, and our work went into the gaps.
- Fewer moving parts wins. One small binary we control replaced a proprietary edge feature without adding a second CDN, a second cache to purge, or a new vendor contract.
- Make cutover reversible. A per-site switch let the team move traffic gradually and step back instantly if anything looked wrong.