When Cindy and I went to Australia, we spent some time in the rain forests on the Queensland coast. One of the natural wonders of this area are the huge strangler vines. They seed in the upper branches of a fig tree and gradually work their way down the tree until they root in the soil. Over many years they grow into fantastic and beautiful shapes, meanwhile strangling and killing the tree that was their host.
Martin Fowler, «Strangler application»
2026 update: The migration idea is still useful, but SSI should not be read as the default architecture for new systems. Today I would first look for a cleaner seam: reverse-proxy routing for whole paths, a gateway, a backend-for-frontend, framework-level server rendering, or ordinary API-backed UI composition. SSI is still valid when you need to replace one fragment inside a legacy HTML response and cannot yet move the whole page. Treat it as a narrow migration tool: only include trusted internal upstreams, set tight timeouts, decide what happens when a fragment fails, measure latency, cache deliberately, and make sure NGINX receives uncompressed HTML before SSI processing.
I once inherited a large and neglected web project that probably had every obvious sign and problem typical of legacy systems. All components of the system had been written in such a way that further extension, change, and maintenance, if they were possible at all, turned into a real nightmare for developers and testers. Even the main modules, such as the core and routing, were tightly bound to the server environment, a million magic numbers, dynamically assigned constants, huge branching functions, and non-obvious hacks. Cyclomatic complexity with three zeros was not something surprising.
Rewriting all this logic while preserving system behavior in every edge case did not seem possible, and neither did developing the existing system further. We decided to use the “Strangler Application” principle, gradually implementing separate components from scratch and replacing the old ones with them. But unfortunately, the entry cost turned out to be too high. Rewriting the server logic that formed a separate block was not a problem, but to integrate the resulting code into the page, we would also have had to rewrite the server logic of all the other blocks on the page, because during initialization the application seriously complicated any clean connection of third-party components. But instead of eating the elephant whole, one should eat the elephant a little at a time :)
In a situation like this, a technology such as SSI can prove useful. It lets you post-process the response body, for example by applying conditional functions or making additional requests. When implementing “Strangler Application,” this can be useful in the following way: the old application loads as usual, but instead of some separate block on the page, there is a call to the new application. Having received such instructions, the web server makes an additional request to the specified address and places the received response where the SSI instruction was. You can compare this with loading a block through an AJAX request, only much faster and completely transparent to the user, because all the magic happens on the server, before the response is sent to the client.
This line in the site code will make the server request /my-new-application-url and “paste” its result into the site code:
<!--# include virtual="/my-new-application-url" -->
In the nginx config, such requests should be enabled:
location / {
...
ssi on;
...
}
It really is that simple. Performance can become a downside of this approach, but in this case the old core loaded in 5-8 seconds, and the new one in about 150 ms, which does not change the weather much.
What I Would Add Today
This trick is useful only if the integration point is explicit and boring.
- Prefer routing a whole URL to the new application when you can. Use SSI for fragments only when the old page still has to render most of the response.
- Keep included endpoints internal. Do not use SSI to include content controlled by users or untrusted upstreams.
- Define failure behavior before production: timeout, fallback HTML, error logging, and alerting.
- Measure the full page, not just the new fragment. A fast fragment can still make a page slow if it adds sequential subrequests.
- Decide cache boundaries deliberately. Fragment composition can make CDN and browser caching less obvious.
- Keep response headers clear. The old page and the included fragment may disagree about cookies, cache headers, content type, and security headers.
- Remember the compression caveat below: NGINX SSI processes the response body it receives. If the upstream sends compressed HTML, SSI directives will not be parsed.
It should also be taken into account that SSI instructions are processed only if nginx receives the response from the upstream in plain form, without compression. If the response is compressed, the instructions will not be executed.