Home › Modern Web › CDN for Remix: Streaming SSR
Modern Web

CDN for Remix: Streaming SSR and Deferred Data Loading at Edge

By Michael Chen, Network Security Engineer • September 7, 2026 • 9 min read

Remix fundamentally changed how we think about server rendering. With React 18 Suspense and the defer() API, Remix can stream HTML to the browser before all data is ready. For users, that means instant shell rendering and progressive hydration. For CDNs, it breaks almost every assumption built over the last decade: that an SSR response is a single, cacheable, atomic string.

If your CDN buffers the entire response before forwarding it, you have negated streaming entirely. If it caches naively, you will serve stale deferred data to the wrong user. This guide covers the correct edge architecture for Remix streaming SSR — how to preserve chunked transfer, cache intelligently, and move deferred loaders to the edge without sacrificing performance or security.

Core Principle: A streaming SSR response is not one resource. It is a shell + a set of deferred chunks with different cache lifetimes, privacy requirements, and origins. Your CDN must treat them as such.

Why Traditional CDN Caching Breaks Streaming SSR

Classic SSR caching is simple: origin renders HTML, CDN stores it with a TTL, subsequent requests are served from cache. This works when rendering is synchronous. Remix streaming is asynchronous by design. Understanding how CDN works at the protocol level explains why this matters.

When a Remix loader returns defer(), the server immediately renders the shell — header, navigation, layout, and any <Suspense> fallbacks — and flushes it. The browser receives <html> and starts downloading CSS, fonts, and client JS while the server is still awaiting promises. As each deferred promise resolves, Remix injects a <script> chunk containing the data and resolves the Suspense boundary on the client.

Three CDN behaviors destroy this flow:

The solution is not to bypass the CDN. It is to reconfigure it for streaming-aware delivery.

How Remix Streaming and Deferred Loading Actually Works

Remix leverages React 18's renderToPipeableStream on the server. The critical APIs are defer() in loaders and <Await> on the component side.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages
// app/routes/dashboard.tsx - Loader with deferred data
import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

export async function loader({ request, context }) {
  // Critical data - blocks shell render
  const user = await getUser(request);
  
  // Deferred data - streams after shell
  return defer({
    user,
    // These promises stream as they resolve
    analytics: getAnalytics(user.id), // 800ms
    recommendations: getRecommendations(user.id), // 1200ms
    notifications: getNotifications(user.id), // 400ms
  });
}

export default function Dashboard() {
  const { user, analytics, recommendations } = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>Welcome {user.name}</h1>
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Await resolve={analytics}>
          {(data) => <AnalyticsChart data={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

On the wire, the response looks like this: first, the shell HTML up to the Suspense fallback is flushed immediately (often within 50-80ms). Then, as each promise resolves, Remix sends an inline script like <script>__remixContext.p(... )</script> that hydrates the boundary. The browser does not need to re-request anything; the data arrives in-stream.

For the CDN, this means Time to First Byte (TTFB) is no longer the full render time. True TTFB is shell flush time. Metrics like serverTiming should differentiate shell;dur=62 vs full;dur=1240 to avoid misleading cache hit ratios.

CDN Architecture for Streaming: Preserve the Stream

To keep Remix streaming intact through the edge, you need a streaming-first CDN path. This is where edge computing diverges from simple pull caching.

Remix Streaming SSR at Edge - Data Flow Diagram showing browser requesting shell from edge CDN, edge streaming shell immediately while fetching deferred data from origin and edge KV. Browser Suspense + Await Edge CDN Chunked + Early Flush Origin / Remix renderToPipeableStream GET /dashboard Cache Miss → Origin Shell (62ms) → Stream Shell Flush Immediate Deferred Chunks analytics • recommendations Edge KV / API Cache per loader key Each deferred promise resolves → inline <script> chunk streamed through edge without buffering
Figure 1: Streaming SSR preserves chunked transfer through the edge. Shell is flushed in ~60ms while deferred data streams as it resolves.

The configuration checklist for any CDN (Cloudflare, Fastly, Akamai, CloudFront) is:

  1. Enable streaming miss: The CDN must forward the origin stream to the client while simultaneously writing to cache, not after. In Fastly this is beresp.do_stream = true; in Cloudflare, disable Rocket Loader and HTML auto-minify for SSR routes.
  2. Disable buffering headers: Origin must send X-Accel-Buffering: no and Cache-Control: no-cache for streaming routes, or CDN-Cache-Control to differentiate edge vs browser. Some CDNs respect no-cache as do-not-buffer signal.
  3. Use chunked encoding correctly: Do not set Content-Length. Ensure origin uses Transfer-Encoding: chunked and the CDN does not re-add length. Verify with curl -v --raw that you see < chunk frames.
  4. Keep connections warm: Use tiered caching and origin keep-alive. Cold TLS + origin connect can add 150-300ms, erasing the streaming benefit for the shell.

Deferred Data Loading at the Edge

The real performance win is not just streaming the shell, but moving deferred loaders to the edge. Instead of the Remix origin awaiting a slow API, the edge can resolve deferred promises from cache, KV, or edge-adjacent APIs.

Consider three patterns:

1. Edge-Cached Deferred Loaders

For non-personalized deferred data like product recommendations, trending lists, or CMS content, cache the promise result at the edge with its own key. The Remix loader still calls defer(), but the function hits edge cache first:

// Edge-aware loader helper
async function getRecommendationsCached(userId, context) {
  const cacheKey = `recs:${userId}:v2`;
  let data = await context.caches.edge.match(cacheKey);
  if (data) return data;
  data = await fetchRecommendationsAPI(userId);
  // Cache deferred chunk separately: 5 min edge, SWR 10 min
  context.caches.edge.put(cacheKey, data, {
    headers: { "CDN-Cache-Control": "max-age=300, stale-while-revalidate=600" }
  });
  return data;
}

This isolates slow data failures. If the recommendations API is down, the shell still streams and the Suspense fallback can show cached stale data or a graceful empty state.

2. Edge Functions for Personalization

For personalized deferred data that cannot be cached publicly, use edge functions to inline the data without hitting origin. Cloudflare Workers or Fastly Compute can run the Remix server itself at the edge, resolving defer() promises via Durable Objects, D1, or Upstash Redis at <10ms. This is the core of modern edge computing for dynamic content — compute moves to data, not the other way around.

3. Split Caching: Shell vs Chunks

Do not cache the full streamed HTML. Instead:

This split model also helps with cost optimization — you avoid re-rendering the entire page when only one deferred loader changes.

Caching Strategies for Streaming Responses

Choosing the right cache directive is critical for security and correctness. A misconfigured Cache-Control can leak user-specific deferred data.

Strategy Use Case Cache Header Example Streaming Safe?
Shell + SWR Anonymous shell, fast data CDN-Cache-Control: max-age=60, stale-while-revalidate=600 Yes, with early flush
Private Deferred User-specific recommendations Cache-Control: private, no-store + Vary: Cookie Yes, no edge cache
Edge-Private Personalized but cacheable per user CDN-Cache-Control: max-age=120, private Yes, with edge key
Static Deferred CMS, product lists CDN-Cache-Control: max-age=300, stale-while-revalidate=3600 Yes, separate chunk cache
No-Cache Stream Authenticated dashboard Cache-Control: no-cache, no-store + X-Accel-Buffering: no Yes, bypass

For authenticated routes, never cache the shell publicly. Use Cache-Control: private and consider CDN-Cache-Control: no-store to ensure the edge does not store it, while still streaming. You can still benefit from edge acceleration via Argo / Dynamic Acceleration (route optimization) even without caching.

Also set Vary correctly. Remix deferred streams often vary on Cookie and Accept-Encoding. If you use Vary: *, you will effectively disable caching. Prefer explicit Vary: Cookie, Accept-Encoding and normalize cookies at the edge (strip analytics cookies) to improve hit rate.

Edge Configuration: Headers, TTLs, and Tiered Caching

A production-ready CDN config for Remix streaming needs four layers:

Origin Headers (Remix Express / Cloudflare Workers)

// entry.server.tsx - Set streaming headers
export default function handleRequest(request, status, headers, remixContext) {
  headers.set("Content-Type", "text/html; charset=utf-8");
  headers.set("Transfer-Encoding", "chunked");
  headers.set("X-Accel-Buffering", "no");
  headers.set("Cache-Control", "no-cache");
  // Edge can cache shell briefly, browser should revalidate
  headers.set("CDN-Cache-Control", "max-age=30, stale-while-revalidate=300");
  headers.set("Vary", "Cookie, Accept-Encoding");
  return renderToPipeableStream(...);
}

CDN Rules

Test your setup with curl -N (no buffer) and WebPageTest with “First Byte” vs “Start Render” comparison. A correctly streaming Remix app should show Start Render 400-700ms before full load, even on 3G. If Start Render equals fully loaded time, your CDN is buffering.

Monitoring and Performance Tuning

Streaming SSR changes what you monitor. Traditional cache hit ratio is misleading because a streaming miss still delivers a fast shell. Track these instead:

Set edge timeout to at least 60s for streaming routes, but keep deferred promises under 3s with Promise.race timeouts to avoid hanging streams. If a deferred promise rejects, Remix will send an error chunk — ensure your ErrorBoundary handles streamed errors gracefully without breaking the HTML stream.

Finally, consider bot and crawler impact. Search engines now support streaming SSR, but some legacy crawlers buffer. Provide a non-streaming fallback via ?_data resource routes or detect crawler UA at the edge and serve a fully rendered, cached snapshot.

In summary, Remix streaming SSR is not incompatible with CDNs — it simply requires a streaming-aware edge. Preserve chunked transfer, cache shell and deferred data separately, move slow loaders to edge KV, and monitor shell TTFB rather than full page time. Done correctly, you get the best of both worlds: sub-100ms shell delivery from the edge and rich, personalized content that streams in without blocking. For teams evaluating cost and performance tradeoffs, this architecture reduces origin compute by 40-60% while improving Core Web Vitals LCP and INP.

Frequently Asked Questions

Can CDNs cache Remix streaming SSR responses?

Yes, but not as a single static asset. CDNs must use chunked transfer encoding, early flush, and tiered caching. The shell (fast data) can be cached briefly at the edge, while deferred chunks are streamed and optionally cached separately via Cache-Control and Surrogate-Key strategies.

Does defer() in Remix work with edge functions?

Absolutely. Remix defer() is ideal for edge deployment because critical data resolves quickly at the edge while slow data (personalization, recommendations) streams later. Edge runtimes like Cloudflare Workers and Fastly Compute support ReadableStream for this pattern.

How do you prevent buffering from breaking streaming SSR?

Disable buffering at every layer: set X-Accel-Buffering: no, Cache-Control: no-cache for the streaming response, and configure your CDN to enable chunked encoding and streaming miss. Also avoid HTML minification or Brotli re-compression that forces full response buffering.

What is the best TTL for deferred data at the edge?

Use short TTLs (30-60s) with stale-while-revalidate of 300-600s for the shell, and separate cache entries for deferred loaders with longer TTLs (5-15 min) based on data volatility. Personalized deferred data should use private, no-store or edge-private caching with Vary on Cookie/Authorization.

References & Standards: HTTP/2 specification · OWASP CDN Security