<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Felix Schober</title>
    <description>The latest articles on DEV Community by Felix Schober (@felixschober).</description>
    <link>https://dev.to/felixschober</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F246867%2F20f7a20e-c54e-4b87-9598-47d035f3e3fc.jpeg</url>
      <title>DEV Community: Felix Schober</title>
      <link>https://dev.to/felixschober</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/felixschober"/>
    <language>en</language>
    <item>
      <title>Why Azure Front Door Made My Next.js App Take 90 Seconds to Load (and How I Fixed It)</title>
      <dc:creator>Felix Schober</dc:creator>
      <pubDate>Sat, 21 Feb 2026 16:17:46 +0000</pubDate>
      <link>https://dev.to/felixschober/why-azure-front-door-made-my-nextjs-app-take-90-seconds-to-load-and-how-i-fixed-it-4kof</link>
      <guid>https://dev.to/felixschober/why-azure-front-door-made-my-nextjs-app-take-90-seconds-to-load-and-how-i-fixed-it-4kof</guid>
      <description>&lt;p&gt;We shipped a Next.js app on Azure Container Apps behind Azure Front Door Premium with Private Link. Standard setup, nothing exotic. Then every page started taking 90 seconds to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom
&lt;/h2&gt;

&lt;p&gt;The HTML document loaded fine. API routes were fast. But every JS chunk would hang for precisely 90 seconds before the browser threw &lt;code&gt;ERR_HTTP2_PROTOCOL_ERROR&lt;/code&gt; — the underlying HTTP/2 stream dying with &lt;code&gt;INTERNAL_ERROR (err 2)&lt;/code&gt;. Not some chunks. All of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; on &lt;strong&gt;Azure Container Apps&lt;/strong&gt; (internal environment, Private Link)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Front Door Premium&lt;/strong&gt; as the CDN/WAF layer&lt;/li&gt;
&lt;li&gt;Three routes: API (&lt;code&gt;/api/*&lt;/code&gt;), static assets (&lt;code&gt;/_next/static/*&lt;/code&gt;), and a catch-all (&lt;code&gt;/*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Next.js config: &lt;code&gt;compress: true&lt;/code&gt; (the default)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The rule you need to remember before reading further:&lt;/strong&gt; never compress at both the origin and the CDN. Pick one. I learned this the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrowing It Down
&lt;/h2&gt;

&lt;p&gt;Origin health probes: 100%. Small responses: fine. The &lt;code&gt;/sign-in&lt;/code&gt; page (78KB SSR) loaded in ~300ms through the catch-all route. Something was wrong specifically with static asset delivery.&lt;/p&gt;

&lt;p&gt;Same JS chunk, with and without &lt;code&gt;Accept-Encoding&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Without gzip — 303ms, full response&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"Total: %{time_total}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://my-fd-endpoint.azurefd.net/_next/static/chunks/app.js"&lt;/span&gt;
Total: 0.303414s

&lt;span class="c"&gt;# With gzip — 90 seconds, incomplete, HTTP/2 stream error&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"Total: %{time_total}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Encoding: gzip"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://my-fd-endpoint.azurefd.net/_next/static/chunks/app.js"&lt;/span&gt;
Total: 90.245256s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same file. Same route. Same origin. The only difference: asking for gzip. I would never have investigated the &lt;code&gt;Accept-Encoding&lt;/code&gt; header without AI pointing me there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The response headers from the broken request
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 200
content-type: application/javascript; charset=UTF-8
content-length: 112049
cache-control: public, max-age=31536000, immutable
content-encoding: gzip
vary: Accept-Encoding
x-cache: TCP_MISS
x-azure-ref: 20260220T195031Z-157f99bd8b842q87hC1CPH...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;content-length: 112049&lt;/code&gt; with &lt;code&gt;content-encoding: gzip&lt;/code&gt;. The origin is telling Front Door: "here's 112KB of gzip data." But curl's &lt;code&gt;size_download&lt;/code&gt; reported only 8,527 bytes received before the HTTP/2 stream died with &lt;code&gt;INTERNAL_ERROR (err 2)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the SSR page didn't break
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/sign-in&lt;/code&gt; SSR page worked fine even with &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; in the request. Compare its response headers to the broken static chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 200
content-type: text/html; charset=utf-8
vary: rsc, next-router-state-tree, next-router-prefetch, ...
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
x-cache: CONFIG_NOCACHE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;content-encoding&lt;/code&gt;. No &lt;code&gt;content-length&lt;/code&gt; (chunked transfer). Based on the response headers, the origin simply didn't gzip the SSR response — even though the client asked for it. The static chunks got &lt;code&gt;content-encoding: gzip&lt;/code&gt;; the SSR page didn't. That's why one broke and the other didn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I tried (all failed)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disabled Front Door compression&lt;/strong&gt; (&lt;code&gt;compression_enabled = false&lt;/code&gt;). Still broken. This was the critical clue: the issue &lt;strong&gt;isn't double compression&lt;/strong&gt;. It's that Front Door cannot properly relay an already-gzip-compressed response from the origin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removed the cache block entirely&lt;/strong&gt;. Still broken. So caching wasn't the cause either.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Switched forwarding protocol to &lt;code&gt;HttpOnly&lt;/code&gt;&lt;/strong&gt;. Still broken. TLS over private link wasn't the issue.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Attempt 1 proved this wasn't about both layers compressing simultaneously. Attempt 2 proved Front Door's cache population wasn't the bottleneck. Attempt 3 ruled out Private Link TLS overhead. What remained: Front Door fundamentally can't relay a gzip-encoded origin response without stalling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;When the origin sends a response with &lt;code&gt;content-encoding: gzip&lt;/code&gt;, something in Front Door's HTTP/2 relay path breaks. I don't know the exact internal detail — Front Door is a black box — but the observable behavior is clear: the data transfer from origin to PoP stalls, and Front Door kills the connection after exactly 90 seconds. That number is &lt;a href="https://learn.microsoft.com/en-us/azure/frontdoor/front-door-faq" rel="noopener noreferrer"&gt;Front Door's non-configurable HTTP keep-alive idle timeout&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This isn't specific to Private Link — it's a &lt;a href="https://learn.microsoft.com/en-us/azure/frontdoor/standard-premium/troubleshoot-compression" rel="noopener noreferrer"&gt;known Front Door behavior&lt;/a&gt;. Microsoft even issued a &lt;a href="https://github.com/envoyproxy/envoy/issues/28828" rel="noopener noreferrer"&gt;Health Advisory&lt;/a&gt; when they tightened HTTP compliance across PoPs. Their Q&amp;amp;A threads (&lt;a href="https://learn.microsoft.com/en-us/answers/questions/5622103/critical-azure-front-door-http-2-protocol-errors-c" rel="noopener noreferrer"&gt;one&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/answers/questions/1855694/unable-to-complete-resource-download-from-azure-fr" rel="noopener noreferrer"&gt;two&lt;/a&gt;) describe the exact same failure and the same fix: disable origin compression.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Disable compression at the origin. Let Front Door compress at the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  next.config.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Front Door compresses at the edge&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Front Door route (Terraform)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_cdn_frontdoor_route"&lt;/span&gt; &lt;span class="s2"&gt;"static"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;query_string_caching_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"UseQueryString"&lt;/span&gt;
    &lt;span class="nx"&gt;compression_enabled&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;content_types_to_compress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"text/html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"text/css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"text/javascript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"application/javascript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"application/x-javascript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"image/svg+xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"font/woff2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the origin sends uncompressed responses. Front Door compresses and caches them at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The General Rule
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never compress at both the origin and the CDN.&lt;/strong&gt; When using Azure Front Door, disable compression at the origin and let Front Door handle it at the edge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Next.js, that means &lt;code&gt;compress: false&lt;/code&gt;. For Express, drop the &lt;code&gt;compression&lt;/code&gt; middleware. For Azure App Service, set &lt;code&gt;WEBSITES_DISABLE_CONTENT_COMPRESSION=1&lt;/code&gt;. For Nginx behind AFD, turn off &lt;code&gt;gzip on&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Diagnostic
&lt;/h2&gt;

&lt;p&gt;If your site is slow behind Azure Front Door, run these two curl commands against the same asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Without compression&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"TTFB: %{time_starttransfer}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Total: %{time_total}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"https://your-fd-endpoint.azurefd.net/your-asset.js"&lt;/span&gt;

&lt;span class="c"&gt;# With compression&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"TTFB: %{time_starttransfer}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Total: %{time_total}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Encoding: gzip"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://your-fd-endpoint.azurefd.net/your-asset.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the first completes in milliseconds and the second hangs for 90 seconds, you've hit this issue. Save the &lt;code&gt;x-azure-ref&lt;/code&gt; header from the broken response — you'll need it if you open a support ticket with Microsoft.&lt;/p&gt;

&lt;p&gt;After applying the fix, a healthy response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 200
content-type: application/javascript; charset=UTF-8
content-length: 41182
cache-control: public, max-age=31536000, immutable
x-cache: TCP_HIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;content-encoding&lt;/code&gt; from the origin. Front Door served it from cache in milliseconds. If you see &lt;code&gt;x-cache: TCP_HIT&lt;/code&gt; and no stall, you're good.&lt;/p&gt;




&lt;p&gt;This took me an embarrassing number of hours to figure out. In hindsight, &lt;code&gt;compress: false&lt;/code&gt; behind a CDN should have been the default from the start — the CDN exists to do this job. Hopefully this saves someone else the debugging session, or, more likely at this point, some AI company will crawl this post and the next time someone asks their model, it'll just know.&lt;/p&gt;

&lt;p&gt;Have you hit this? What's your worst CDN debugging story?&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/frontdoor/standard-premium/troubleshoot-compression" rel="noopener noreferrer"&gt;Troubleshoot File Compression - Azure Front Door (Microsoft Learn)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/answers/questions/1855694/unable-to-complete-resource-download-from-azure-fr" rel="noopener noreferrer"&gt;Unable to complete resource download with Accept-Encoding: gzip (Microsoft Q&amp;amp;A)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/answers/questions/5622103/critical-azure-front-door-http-2-protocol-errors-c" rel="noopener noreferrer"&gt;Azure Front Door HTTP/2 Protocol Errors on Static Assets (Microsoft Q&amp;amp;A)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/envoyproxy/envoy/issues/28828" rel="noopener noreferrer"&gt;Envoy + Azure CDN gzip range request issue (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/answers/questions/1338000/http2-protocol-error-with-azure-cdn" rel="noopener noreferrer"&gt;HTTP2_PROTOCOL_ERROR with Azure CDN (Microsoft Q&amp;amp;A)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/frontdoor/front-door-faq" rel="noopener noreferrer"&gt;Azure Front Door FAQ - Timeouts (Microsoft Learn)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/frontdoor/standard-premium/how-to-compression" rel="noopener noreferrer"&gt;Improve performance by compressing files (Microsoft Learn)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>debug</category>
    </item>
  </channel>
</rss>
