HTTP Streaming: Enhancing Web Performance and User Experience with Airbnb's Approach

Arpit Bhayani

Arpit Bhayani

Sep 07, 2024 • 8 min read

Play

HTTP Streaming: Enhancing Web Performance and User Experience

HTTP is the fundamental language of the internet, and while most APIs compile an entire response before sending it, HTTP streaming offers an alternative approach. This document explores HTTP streaming, its implementation, advantages, and how companies like Airbnb leverage it to deliver superior user experiences.

Buffered vs. Streaming Responses

There are two primary ways to send responses from a server to a client:

  1. Buffered Responses: In a typical scenario, the server receives a request, processes it (e.g., queries a database), compiles the entire response, and then sends it to the client. The client receives the complete response, often knowing its exact length via the Content-Length header, before processing begins.
  2. Streaming Responses: In contrast, streaming responses allow the server to send data in chunks as it becomes available, without knowing the final length of the response upfront. The client receives and can begin processing these chunks immediately.

When to Use HTTP Streaming?

While buffered responses are suitable for small, predictable data, HTTP streaming shines in scenarios where:

  • The response is large or its length is unknown (e.g., Large Language Model (LLM) applications like ChatGPT, where text is generated progressively).
  • Improving user experience by displaying partial content as soon as possible is critical.

It’s important to note that HTTP streaming is distinct from WebSockets. WebSockets are a separate protocol for full-duplex communication, whereas HTTP streaming utilizes standard HTTP mechanisms.

How HTTP Streaming Works

The core mechanism behind HTTP streaming relies on the Transfer-Encoding: chunked header. When this header is present in an HTTP response, it signals to the client that the server will send the response body in a series of chunks. Crucially, the Content-Length header is omitted, as the total size is unknown.

The Chunked Transfer Encoding Process:

  1. Server receives request.
  2. Server begins processing.
  3. As data chunks become ready, the server sends them to the client, each prefixed with its size in hexadecimal, followed by the chunk data, and terminated by \r .
  4. Client receives chunks and can start processing them immediately.
  5. Server sends a zero-length chunk (0\r\n\r ) to indicate the end of the response.

What the client does with the received chunks depends on the use case. It might accumulate the data until the full response is received, or it might process and render partial data as it arrives (e.g., appending text in an LLM interface).

Implementation Examples

Modern web frameworks simplify the implementation of HTTP streaming:

  • Node.js (Express): Frameworks like Express allow you to listen for data chunks on the request object (req.on('data', chunk => { ... })) or send chunks using res.write() and res.end().
  • Python (Flask): In Flask, you can return a generator function from a route handler. Flask automatically handles setting the Transfer-Encoding: chunked header and streaming the output of the generator.

Practical Use Case: Deployment Log Streaming

Imagine a scenario where you want to stream deployment logs to a frontend. A Python script on the server could read from a deployment log file and, as new lines are appended, stream those lines as chunks to the client, providing real-time updates.

Key Advantages of HTTP Streaming

HTTP streaming offers two significant advantages:

  1. Low and Consistent Memory Usage: On the sender side (server), memory usage remains constant because the entire response doesn’t need to be buffered in memory before sending. Instead, data is processed and sent in smaller chunks, allowing the server to handle responses larger than its available memory.
    • Example: Sending a 1MB file. A buffered approach would require 1MB of memory to hold the entire file. A streaming approach sends 1KB chunks, keeping memory usage low and consistent.
  2. Receiver Can Start Processing Immediately: The client doesn’t have to wait for the entire response to arrive. This is crucial for crafting excellent user experiences, especially with long-running operations.
    • Example: An LLM might take 15 seconds to generate a full response. With streaming, the client can display text as it’s generated, keeping the user engaged rather than staring at a blank screen.

While WebSockets can achieve similar interactivity, HTTP streaming often provides a simpler solution for many use cases without introducing the overhead and complexity of a separate protocol.

Airbnb’s Real-World Application of HTTP Streaming

Airbnb leverages HTTP streaming to significantly improve its First Contentful Paint (FCP), a key metric for user experience. FCP measures the time until the first piece of content is rendered on the screen, signaling to the user that something is happening.

1. Early Flush of External Resources

Airbnb’s frontend, built with Express/Node.js and React, splits the HTML response into multiple chunks to achieve early flushing:

  • Initial Chunk (Early Flush): Upon receiving a request, the server immediately streams the head tag of the HTML. This head typically contains critical resources like CSS files, JavaScript files, inline scripts, and meta tags. This allows the client (browser) to start downloading and processing these essential resources without waiting for the entire page body to be rendered.
  • Subsequent Chunks: The server then proceeds to render the main body content, which might involve database queries or other time-consuming operations. As these parts become ready, they are streamed to the client.

Implementation Details:

Airbnb breaks the page into React components. They render the HTML for the head and then use res.send() to stream it. Subsequently, they render and stream the body content, followed by a late head chunk (e.g., for analytics scripts like Mixpanel or Amplitude), and finally close the HTML tag to ensure a valid, complete document.

This approach ensures that users see a progressively loading page, starting with the navigation bar, then middle sections, profile photos, and so on, rather than waiting for a complete page to appear all at once.

2. Minimizing Load Times with Defer Data

Beyond early flushing, Airbnb addresses another performance bottleneck: client-side API calls that block rendering after the initial HTML is received.

  • The Problem: After the browser renders the initial HTML, it might encounter JavaScript code that triggers client-side API calls to fetch data needed for specific components (e.g., a user profile). This introduces a waterfall effect: render HTML -> make API call -> wait for response -> render data. During this waiting period, users might see loading spinners.
  • The Solution: defer-data Chunk: Airbnb introduced a special defer-data chunk. This chunk is streamed after the basic HTML layout (body) has been sent. It contains the JSON responses of the API calls that would typically be made from the client side.

How it Works:

  1. The server sends the early HTML chunks, including the basic layout of the page.
  2. The server then computes the data for client-side API calls on the server itself.
  3. This computed data is encapsulated as an application/json payload within a defer-data chunk and streamed to the client.
  4. On the client side, a MutationObserver is used to detect the arrival of this defer-data chunk. (A MutationObserver allows JavaScript to react to changes in the DOM tree.)
  5. Once detected, the client parses the JSON data from the defer-data chunk and uses it to hydrate its global state (e.g., in React). This directly populates the UI with actual data, bypassing the need for an additional client-to-server API roundtrip.

This technique ensures that the user sees a meaningful interface (even if it’s a skeleton or default data) almost immediately, and then the actual data seamlessly replaces placeholders without visible loading states, further minimizing perceived load times.

Implementation Challenges

While powerful, HTTP streaming can introduce challenges, particularly in production environments:

  • Reverse Proxy Buffering: Common reverse proxies like Nginx often buffer responses by default. This means that even if your application server is streaming data in chunks, the proxy might accumulate the entire response before forwarding it to the client, negating the benefits of streaming.
  • Solution: To enable proper HTTP streaming, it’s crucial to configure your reverse proxy to disable buffering for the specific requests that are meant to be streamed. For Nginx, this typically involves setting proxy_buffering off; for the relevant location or server block.

Conclusion

HTTP streaming is a valuable technique for optimizing web applications, particularly for large responses or when enhancing user experience through progressive rendering. By understanding its mechanics and leveraging strategies like early flushing and deferred data, developers can significantly improve metrics like First Contentful Paint and create more engaging, responsive web experiences, as demonstrated by Airbnb’s successful implementation.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses