
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Tue, 14 Apr 2026 21:33:42 GMT</lastBuildDate>
        <item>
            <title><![CDATA[We deserve a better streams API for JavaScript]]></title>
            <link>https://blog.cloudflare.com/a-better-web-streams-api/</link>
            <pubDate>Fri, 27 Feb 2026 06:00:00 GMT</pubDate>
            <description><![CDATA[ The Web streams API has become ubiquitous in JavaScript runtimes but was designed for a different era. Here's what a modern streaming API could (should?) look like. ]]></description>
            <content:encoded><![CDATA[ <p>Handling data in streams is fundamental to how we build applications. To make streaming work everywhere, the <a href="https://streams.spec.whatwg.org/"><u>WHATWG Streams Standard</u></a> (informally known as "Web streams") was designed to establish a common API to work across browsers and servers. It shipped in browsers, was adopted by Cloudflare Workers, Node.js, Deno, and Bun, and became the foundation for APIs like <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"><u>fetch()</u></a>. It's a significant undertaking, and the people who designed it were solving hard problems with the constraints and tools they had at the time.</p><p>But after years of building on Web streams – implementing them in both Node.js and Cloudflare Workers, debugging production issues for customers and runtimes, and helping developers work through far too many common pitfalls – I've come to believe that the standard API has fundamental usability and performance issues that cannot be fixed easily with incremental improvements alone. The problems aren't bugs; they're consequences of design decisions that may have made sense a decade ago, but don't align with how JavaScript developers write code today.</p><p>This post explores some of the fundamental issues I see with Web streams and presents an alternative approach built around JavaScript language primitives that demonstrate something better is possible. </p><p>In benchmarks, this alternative can run anywhere between 2x to <i>120x</i> faster than Web streams in every runtime I've tested it on (including Cloudflare Workers, Node.js, Deno, Bun, and every major browser). The improvements are not due to clever optimizations, but fundamentally different design choices that more effectively leverage modern JavaScript language features. I'm not here to disparage the work that came before; I'm here to start a conversation about what can potentially come next.</p>
    <div>
      <h2>Where we're coming from</h2>
      <a href="#where-were-coming-from">
        
      </a>
    </div>
    <p>The Streams Standard was developed between 2014 and 2016 with an ambitious goal to provide "APIs for creating, composing, and consuming streams of data that map efficiently to low-level I/O primitives." Before Web streams, the web platform had no standard way to work with streaming data.</p><p>Node.js already had its own <a href="https://nodejs.org/api/stream.html"><u>streaming API</u></a> at the time that was ported to also work in browsers, but WHATWG chose not to use it as a starting point given that it is chartered to only consider the needs of Web browsers. Server-side runtimes only adopted Web streams later, after Cloudflare Workers and Deno each emerged with first-class Web streams support and cross-runtime compatibility became a priority.</p><p>The design of Web streams predates async iteration in JavaScript. The <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of"><code><u>for await...of</u></code></a> syntax didn't land until <a href="https://262.ecma-international.org/9.0/"><u>ES2018</u></a>, two years after the Streams Standard was initially finalized. This timing meant the API couldn't initially leverage what would eventually become the idiomatic way to consume asynchronous sequences in JavaScript. Instead, the spec introduced its own reader/writer acquisition model, and that decision rippled through every aspect of the API.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3X0niHShBlgF4LlpWYB7eC/f0bbf35f12ecc98a3888e6e3835acf3a/1.png" />
          </figure>
    <div>
      <h4>Excessive ceremony for common operations</h4>
      <a href="#excessive-ceremony-for-common-operations">
        
      </a>
    </div>
    <p>The most common task with streams is reading them to completion. Here's what that looks like with Web streams:</p>
            <pre><code>// First, we acquire a reader that gives an exclusive lock
// on the stream...
const reader = stream.getReader();
const chunks = [];
try {
  // Second, we repeatedly call read and await on the returned
  // promise to either yield a chunk of data or indicate we're
  // done.
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    chunks.push(value);
  }
} finally {
  // Finally, we release the lock on the stream
  reader.releaseLock();
}</code></pre>
            <p>You might assume this pattern is inherent to streaming. It isn't. The reader acquisition, the lock management, and the <code>{ value, done }</code> protocol are all just design choices, not requirements. They are artifacts of how and when the Web streams spec was written. Async iteration exists precisely to handle sequences that arrive over time, but async iteration did not yet exist when the streams specification was written. The complexity here is pure API overhead, not fundamental necessity.</p><p>Consider the alternative approach now that Web streams do support <code>for await...of</code>:</p>
            <pre><code>const chunks = [];
for await (const chunk of stream) {
  chunks.push(chunk);
}</code></pre>
            <p>This is better in that there is far less boilerplate, but it doesn't solve everything. Async iteration was retrofitted onto an API that wasn't designed for it, and it shows. Features like <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader"><u>BYOB (bring your own buffer)</u></a> reads aren't accessible through iteration. The underlying complexity of readers, locks, and controllers are still there, just hidden. When something does go wrong, or when additional features of the API are needed, developers find themselves back in the weeds of the original API, trying to understand why their stream is "locked" or why <code>releaseLock()</code> didn't do what they expected or hunting down bottlenecks in code they don't control.</p>
    <div>
      <h4>The locking problem</h4>
      <a href="#the-locking-problem">
        
      </a>
    </div>
    <p>Web streams use a locking model to prevent multiple consumers from interleaving reads. When you call <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader"><code><u>getReader()</u></code></a>, the stream becomes locked. While locked, nothing else can read from the stream directly, pipe it, or even cancel it – only the code that is actually holding the reader can.</p><p>This sounds reasonable until you see how easily it goes wrong:</p>
            <pre><code>async function peekFirstChunk(stream) {
  const reader = stream.getReader();
  const { value } = await reader.read();
  // Oops — forgot to call reader.releaseLock()
  // And the reader is no longer available when we return
  return value;
}

const first = await peekFirstChunk(stream);
// TypeError: Cannot obtain lock — stream is permanently locked
for await (const chunk of stream) { /* never runs */ }</code></pre>
            <p>Forgetting <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/releaseLock"><code><u>releaseLock()</u></code></a> permanently breaks the stream. The <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/locked"><code><u>locked</u></code></a><code> </code>property tells you that a stream is locked, but not why, by whom, or whether the lock is even still usable. <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/pipeTo"><u>Piping</u></a> internally acquires locks, making streams unusable during pipe operations in ways that aren't obvious.</p><p>The semantics around releasing locks with pending reads were also unclear for years. If you called read() but didn't await it, then called releaseLock(), what happened? The spec was recently clarified to cancel pending reads on lock release – but implementations varied, and code that relied on the previous unspecified behavior can break.</p><p>That said, it's important to recognize that locking in itself is not bad. It does, in fact, serve an important purpose to ensure that applications properly and orderly consume or produce data. The key challenge is with the original manual implementation of it using APIs like <code>getReader() </code>and <code>releaseLock()</code>. With the arrival of automatic lock and reader management with async iterables, dealing with locks from the users point of view became a lot easier.</p><p>For implementers, the locking model adds a fair amount of non-trivial internal bookkeeping. Every operation must check lock state, readers must be tracked, and the interplay between locks, cancellation, and error states creates a matrix of edge cases that must all be handled correctly.</p>
    <div>
      <h4>BYOB: complexity without payoff</h4>
      <a href="#byob-complexity-without-payoff">
        
      </a>
    </div>
    <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader"><u>BYOB (bring your own buffer)</u></a> reads were designed to let developers reuse memory buffers when reading from streams, an important optimization intended for high-throughput scenarios. The idea is sound: instead of allocating new buffers for each chunk, you provide your own buffer and the stream fills it.</p><p>In practice, (and yes, there are always exceptions to be found) BYOB is rarely used to any measurable benefit. The API is substantially more complex than default reads, requiring a separate reader type (<code>ReadableStreamBYOBReader</code>) and other specialized classes (e.g. <code>ReadableStreamBYOBRequest</code>), careful buffer lifecycle management, and understanding of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#transferring_arraybuffers"><code><u>ArrayBuffer</u></code><u> detachment</u></a> semantics. When you pass a buffer to a BYOB read, the buffer becomes detached – transferred to the stream – and you get back a different view over potentially different memory. This transfer-based model is error-prone and confusing:</p>
            <pre><code>const reader = stream.getReader({ mode: 'byob' });
const buffer = new ArrayBuffer(1024);
let view = new Uint8Array(buffer);

const result = await reader.read(view);
// 'view' should now be detached and unusable
// (it isn't always in every impl)
// result.value is a NEW view, possibly over different memory
view = result.value; // Must reassign</code></pre>
            <p>BYOB also can't be used with async iteration or TransformStreams, so developers who want zero-copy reads are forced back into the manual reader loop.</p><p>For implementers, BYOB adds significant complexity. The stream must track pending BYOB requests, handle partial fills, manage buffer detachment correctly, and coordinate between the BYOB reader and the underlying source. The <a href="https://github.com/web-platform-tests/wpt/tree/master/streams/readable-byte-streams"><u>Web Platform Tests for readable byte streams</u></a> include dedicated test files just for BYOB edge cases: detached buffers, bad views, response-after-enqueue ordering, and more.</p><p>BYOB ends up being complex for both users and implementers, yet sees little adoption in practice. Most developers stick with default reads and accept the allocation overhead.</p><p>Most userland implementations of custom ReadableStream instances do not typically bother with all the ceremony required to correctly implement both default and BYOB read support in a single stream – and for good reason. It's difficult to get right and most of the time consuming code is typically going to fallback on the default read path. The example below shows what a "correct" implementation would need to do. It's big, complex, and error prone, and not a level of complexity that the typical developer really wants to have to deal with:</p>
            <pre><code>new ReadableStream({
    type: 'bytes',
    
    async pull(controller: ReadableByteStreamController) {      
      if (offset &gt;= totalBytes) {
        controller.close();
        return;
      }
      
      // Check for BYOB request FIRST
      const byobRequest = controller.byobRequest;
      
      if (byobRequest) {
        // === BYOB PATH ===
        // Consumer provided a buffer - we MUST fill it (or part of it)
        const view = byobRequest.view!;
        const bytesAvailable = totalBytes - offset;
        const bytesToWrite = Math.min(view.byteLength, bytesAvailable);
        
        // Create a view into the consumer's buffer and fill it
        // not critical but safer when bytesToWrite != view.byteLength
        const dest = new Uint8Array(
          view.buffer,
          view.byteOffset,
          bytesToWrite
        );
        
        // Fill with sequential bytes (our "data source")
        // Can be any thing here that writes into the view
        for (let i = 0; i &lt; bytesToWrite; i++) {
          dest[i] = (offset + i) &amp; 0xFF;
        }
        
        offset += bytesToWrite;
        
        // Signal how many bytes we wrote
        byobRequest.respond(bytesToWrite);
        
      } else {
        // === DEFAULT READER PATH ===
        // No BYOB request - allocate and enqueue a chunk
        const bytesAvailable = totalBytes - offset;
        const chunkSize = Math.min(1024, bytesAvailable);
        
        const chunk = new Uint8Array(chunkSize);
        for (let i = 0; i &lt; chunkSize; i++) {
          chunk[i] = (offset + i) &amp; 0xFF;
        }
        
        offset += chunkSize;
        controller.enqueue(chunk);
      }
    },
    
    cancel(reason) {
      console.log('Stream canceled:', reason);
    }
  });</code></pre>
            <p>When a host runtime provides a byte-oriented ReadableStream from the runtime itself, for instance, as the <code>body </code>of a fetch <code>Response</code>, it is often far easier for the runtime itself to provide an optimized implementation of BYOB reads, but those still need to be capable of handling both default and BYOB reading patterns and that requirement brings with it a fair amount of complexity.</p>
    <div>
      <h4>Backpressure: good in theory, broken in practice</h4>
      <a href="#backpressure-good-in-theory-broken-in-practice">
        
      </a>
    </div>
    <p>Backpressure – the ability for a slow consumer to signal a fast producer to slow down – is a first-class concept in Web streams. In theory. In practice, the model has some serious flaws.</p><p>The primary signal is <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController/desiredSize"><code><u>desiredSize</u></code></a> on the controller. It can be positive (wants data), zero (at capacity), negative (over capacity), or null (closed). Producers are supposed to check this value and stop enqueueing when it's not positive. But there's nothing enforcing this: <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController/enqueue"><code><u>controller.enqueue()</u></code></a> always succeeds, even when desiredSize is deeply negative.</p>
            <pre><code>new ReadableStream({
  start(controller) {
    // Nothing stops you from doing this
    while (true) {
      controller.enqueue(generateData()); // desiredSize: -999999
    }
  }
});</code></pre>
            <p>Stream implementations can and do ignore backpressure; and some spec-defined features explicitly break backpressure. <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/tee"><code><u>tee()</u></code></a>, for instance, creates two branches from a single stream. If one branch reads faster than the other, data accumulates in an internal buffer with no limit. A fast consumer can cause unbounded memory growth while the slow consumer catches up, and there's no way to configure this or opt out beyond canceling the slower branch.</p><p>Web streams do provide clear mechanisms for tuning backpressure behavior in the form of the <code>highWaterMark</code> option and customizable size calculations, but these are just as easy to ignore as <code>desiredSize</code>, and many applications simply fail to pay attention to them.</p><p>The same issues exist on the <code>WritableStream</code> side. A <code>WritableStream</code> has a <code>highWaterMark</code> and <code>desiredSize</code>. There is a <code>writer.ready</code> promise that producers of data are supposed to pay attention but often don't.</p>
            <pre><code>const writable = getWritableStreamSomehow();
const writer = writable.getWriter();

// Producers are supposed to wait for the writer.ready
// It is a promise that, when resolves, indicates that
// the writables internal backpressure is cleared and
// it is ok to write more data
await writer.ready;
await writer.write(...);</code></pre>
            <p>For implementers, backpressure adds complexity without providing guarantees. The machinery to track queue sizes, compute <code>desiredSize</code>, and invoke <code>pull()</code> at the right times must all be implemented correctly. However, since these signals are advisory, all that work doesn't actually prevent the problems backpressure is supposed to solve.</p>
    <div>
      <h4>The hidden cost of promises</h4>
      <a href="#the-hidden-cost-of-promises">
        
      </a>
    </div>
    <p>The Web streams spec requires promise creation at numerous points, often in hot paths and often invisible to users. Each <code>read()</code> call doesn't just return a promise; internally, the implementation creates additional promises for queue management, <code>pull()</code> coordination, and backpressure signaling.</p><p>This overhead is mandated by the spec's reliance on promises for buffer management, completion, and backpressure signals. While some of it is implementation-specific, much of it is unavoidable if you're following the spec as written. For high-frequency streaming – video frames, network packets, real-time data – this overhead is significant.</p><p>The problem compounds in pipelines. Each <code>TransformStream</code> adds another layer of promise machinery between source and sink. The spec doesn't define synchronous fast paths, so even when data is available immediately, the promise machinery still runs.</p><p>For implementers, this promise-heavy design constrains optimization opportunities. The spec mandates specific promise resolution ordering, making it difficult to batch operations or skip unnecessary async boundaries without risking subtle compliance failures. There are many hidden internal optimizations that implementers do make but these can be complicated and difficult to get right.</p><p>While I was writing this blog post, Vercel's Malte Ubl published their own <a href="https://vercel.com/blog/we-ralph-wiggumed-webstreams-to-make-them-10x-faster"><u>blog post</u></a> describing some research work Vercel has been doing around improving the performance of Node.js' Web streams implementation. In that post they discuss the same fundamental performance optimization problem that every implementation of Web streams face:</p><blockquote><p>"Or consider pipeTo(). Each chunk passes through a full Promise chain: read, write, check backpressure, repeat. An {value, done} result object is allocated per read. Error propagation creates additional Promise branches.</p><p>None of this is wrong. These guarantees matter in the browser where streams cross security boundaries, where cancellation semantics need to be airtight, where you do not control both ends of a pipe. But on the server, when you are piping React Server Components through three transforms at 1KB chunks, the cost adds up.</p><p>We benchmarked native WebStream pipeThrough at 630 MB/s for 1KB chunks. Node.js pipeline() with the same passthrough transform: ~7,900 MB/s. That is a 12x gap, and the difference is almost entirely Promise and object allocation overhead." 
- Malte Ubl, <a href="https://vercel.com/blog/we-ralph-wiggumed-webstreams-to-make-them-10x-faster"><u>https://vercel.com/blog/we-ralph-wiggumed-webstreams-to-make-them-10x-faster</u></a></p></blockquote><p>As part of their research, they have put together a set of proposed improvements for Node.js' Web streams implementation that will eliminate promises in certain code paths which can yield a significant performance boost up to 10x faster, which only goes to prove the point: promises, while useful, add significant overhead. As one of the core maintainers of Node.js, I am looking forward to helping Malte and the folks at Vercel get their proposed improvements landed!</p><p>In a recent update made to Cloudflare Workers, I made similar kinds of modifications to an internal data pipeline that reduced the number of JavaScript promises created in certain application scenarios by up to 200x. The result is several orders of magnitude improvement in performance in those applications.</p>
    <div>
      <h3>Real-world failures</h3>
      <a href="#real-world-failures">
        
      </a>
    </div>
    
    <div>
      <h4>Exhausting resources with unconsumed bodies</h4>
      <a href="#exhausting-resources-with-unconsumed-bodies">
        
      </a>
    </div>
    <p>When <code>fetch()</code> returns a response, the body is a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/body"><code><u>ReadableStream</u></code></a>. If you only check the status and don't consume or cancel the body, what happens? The answer varies by implementation, but a common outcome is resource leakage.</p>
            <pre><code>async function checkEndpoint(url) {
  const response = await fetch(url);
  return response.ok; // Body is never consumed or cancelled
}

// In a loop, this can exhaust connection pools
for (const url of urls) {
  await checkEndpoint(url);
}</code></pre>
            <p>This pattern has caused connection pool exhaustion in Node.js applications using <a href="https://nodejs.org/api/globals.html#fetch"><u>undici</u></a> (the <code>fetch() </code>implementation built into Node.js), and similar issues have appeared in other runtimes. The stream holds a reference to the underlying connection, and without explicit consumption or cancellation, the connection may linger until garbage collection – which may not happen soon enough under load.</p><p>The problem is compounded by APIs that implicitly create stream branches. <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/clone"><code><u>Request.clone()</u></code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/clone"><code><u>Response.clone()</u></code></a> perform implicit <code>tee()</code> operations on the body stream – a detail that's easy to miss. Code that clones a request for logging or retry logic may unknowingly create branched streams that need independent consumption, multiplying the resource management burden.</p><p>Now, to be certain, these types of issues <i>are</i> implementation bugs. The connection leak was definitely something that undici needed to fix in its own implementation, but the complexity of the specification does not make dealing with these types of issues easy.</p><blockquote><p>"Cloning streams in Node.js's fetch() implementation is harder than it looks. When you clone a request or response body, you're calling tee() - which splits a single stream into two branches that both need to be consumed. If one consumer reads faster than the other, data buffers unbounded in memory waiting for the slow branch. If you don't properly consume both branches, the underlying connection leaks. The coordination required between two readers sharing one source makes it easy to accidentally break the original request or exhaust connection pools. It's a simple API call with complex underlying mechanics that are difficult to get right." - Matteo Collina, Ph.D. - Platformatic Co-Founder &amp; CTO, Node.js Technical Steering Committee Chair</p></blockquote>
    <div>
      <h4>Falling headlong off the tee() memory cliff</h4>
      <a href="#falling-headlong-off-the-tee-memory-cliff">
        
      </a>
    </div>
    <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/tee"><code><u>tee()</u></code></a> splits a stream into two branches. It seems straightforward, but the implementation requires buffering: if one branch is read faster than the other, the data must be held somewhere until the slower branch catches up.</p>
            <pre><code>const [forHash, forStorage] = response.body.tee();

// Hash computation is fast
const hash = await computeHash(forHash);

// Storage write is slow — meanwhile, the entire stream
// may be buffered in memory waiting for this branch
await writeToStorage(forStorage);</code></pre>
            <p>The spec does not mandate buffer limits for <code>tee()</code>. And to be fair, the spec allows implementations to implement the actual internal mechanisms for <code>tee()</code>and other APIs in any way they see fit so long as the observable normative requirements of the specification are met. But if an implementation chooses to implement <code>tee()</code> in the specific way described by the streams specification, then <code>tee()</code> will come with a built-in memory management issue that is difficult to work around.</p><p>Implementations have had to develop their own strategies for dealing with this. Firefox initially used a linked-list approach that led to O<code>(n)</code> memory growth proportional to the consumption rate difference. In Cloudflare Workers, we opted to implement a shared buffer model where backpressure is signaled by the slowest consumer rather than the fastest.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5cl4vqYfaHaVXiHjLSXv0a/03a0b9fe4c9c0594e181ffee43b63998/2.png" />
          </figure>
    <div>
      <h4>Transform backpressure gaps</h4>
      <a href="#transform-backpressure-gaps">
        
      </a>
    </div>
    <p><code>TransformStream</code> creates a <code>readable/writable</code> pair with processing logic in between. The <code>transform()</code> function executes on <i>write</i>, not on read. Processing of the transform happens eagerly as data arrives, regardless of whether any consumer is ready. This causes unnecessary work when consumers are slow, and the backpressure signaling between the two sides has gaps that can cause unbounded buffering under load. The expectation in the spec is that the producer of the data being transformed is paying attention to the <code>writer.ready</code> signal on the writable side of the transform but quite often producers just simply ignore it.</p><p>If the transform's <code>transform() </code>operation is synchronous and always enqueues output immediately, it never signals backpressure back to the writable side even when the downstream consumer is slow. This is a consequence of the spec design that many developers completely overlook. In browsers, where there's only a single user and typically only a small number of stream pipelines active at any given time, this type of foot gun is often of no consequence, but it has a major impact on server-side or edge performance in runtimes that serve thousands of concurrent requests.</p>
            <pre><code>const fastTransform = new TransformStream({
  transform(chunk, controller) {
    // Synchronously enqueue — this never applies backpressure
    // Even if the readable side's buffer is full, this succeeds
    controller.enqueue(processChunk(chunk));
  }
});

// Pipe a fast source through the transform to a slow sink
fastSource
  .pipeThrough(fastTransform)
  .pipeTo(slowSink);  // Buffer grows without bound</code></pre>
            <p>What TransformStreams are supposed to do is check for backpressure on the controller and use promises to communicate that back to the writer:</p>
            <pre><code>const fastTransform = new TransformStream({
  async transform(chunk, controller) {
    if (controller.desiredSize &lt;= 0) {
      // Wait on the backpressure to clear somehow
    }

    controller.enqueue(processChunk(chunk));
  }
});</code></pre>
            <p>A difficulty here, however, is that the <code>TransformStreamDefaultController</code> does not have a ready promise mechanism like Writers do; so the <code>TransformStream</code> implementation would need to implement a polling mechanism to periodically check when <code>controller.desiredSize</code> becomes positive again.</p><p>The problem gets worse in pipelines. When you chain multiple transforms – say, parse, transform, then serialize – each <code>TransformStream</code> has its own internal readable and writable buffers. If implementers follow the spec strictly, data cascades through these buffers in a push-oriented fashion: the source pushes to transform A, which pushes to transform B, which pushes to transform C, each accumulating data in intermediate buffers before the final consumer has even started pulling. With three transforms, you can have six internal buffers filling up simultaneously.</p><p>Developers using the streams API are expected to remember to use options like <code>highWaterMark</code> when creating their sources, transforms, and writable destinations but often they either forget or simply choose to ignore it.</p>
            <pre><code>source
  .pipeThrough(parse)      // buffers filling...
  .pipeThrough(transform)  // more buffers filling...
  .pipeThrough(serialize)  // even more buffers...
  .pipeTo(destination);    // consumer hasn't started yet</code></pre>
            <p>Implementations have found ways to optimize transform pipelines by collapsing identity transforms, short-circuiting non-observable paths, deferring buffer allocation, or falling back to native code that does not run JavaScript at all. Deno, Bun, and Cloudflare Workers have all successfully implemented "native path" optimizations that can help eliminate much of the overhead, and Vercel's recent <a href="https://vercel.com/blog/we-ralph-wiggumed-webstreams-to-make-them-10x-faster"><u>fast-webstreams</u></a> research is working on similar optimizations for Node.js. But the optimizations themselves add significant complexity and still can't fully escape the inherently push-oriented model that TransformStream uses.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/64FcAUPYrTvOSYOPoT2FkR/cc91e0d32dd47320e8ac9d6f431a2fda/3.png" />
          </figure>
    <div>
      <h4>GC thrashing in server-side rendering</h4>
      <a href="#gc-thrashing-in-server-side-rendering">
        
      </a>
    </div>
    <p>Streaming server-side rendering (SSR) is a particularly painful case. A typical SSR stream might render thousands of small HTML fragments, each passing through the streams machinery:</p>
            <pre><code>// Each component enqueues a small chunk
function renderComponent(controller) {
  controller.enqueue(encoder.encode(`&lt;div&gt;${content}&lt;/div&gt;`));
}

// Hundreds of components = hundreds of enqueue calls
// Each one triggers promise machinery internally
for (const component of components) {
  renderComponent(controller);  // Promises created, objects allocated
}</code></pre>
            <p>Every fragment means promises created for <code>read()</code> calls, promises for backpressure coordination, intermediate buffer allocations, and <code>{ value, done } </code>result objects – most of which become garbage almost immediately.</p><p>Under load, this creates GC pressure that can devastate throughput. The JavaScript engine spends significant time collecting short-lived objects instead of doing useful work. Latency becomes unpredictable as GC pauses interrupt request handling. I've seen SSR workloads where garbage collection accounts for a substantial portion (up to and beyond 50%) of total CPU time per request. That's time that could be spent actually rendering content.</p><p>The irony is that streaming SSR is supposed to improve performance by sending content incrementally. But the overhead of the streams machinery can negate those gains, especially for pages with many small components. Developers sometimes find that buffering the entire response is actually faster than streaming through Web streams, defeating the purpose entirely.</p>
    <div>
      <h3>The optimization treadmill</h3>
      <a href="#the-optimization-treadmill">
        
      </a>
    </div>
    <p>To achieve usable performance, every major runtime has resorted to non-standard internal optimizations for Web streams. Node.js, Deno, Bun, and Cloudflare Workers have all developed their own workarounds. This is particularly true for streams wired up to system-level I/O, where much of the machinery is non-observable and can be short-circuited.</p><p>Finding these optimization opportunities can itself be a significant undertaking. It requires end-to-end understanding of the spec to identify which behaviors are observable and which can safely be elided. Even then, whether a given optimization is actually spec-compliant is often unclear. Implementers must make judgment calls about which semantics they can relax without breaking compatibility. This puts enormous pressure on runtime teams to become spec experts just to achieve acceptable performance.</p><p>These optimizations are difficult to implement, frequently error-prone, and lead to inconsistent behavior across runtimes. Bun's "<a href="https://bun.sh/docs/api/streams#direct-readablestream"><u>Direct Streams</u></a>" optimization takes a deliberately and observably non-standard approach, bypassing much of the spec's machinery entirely. Cloudflare Workers' <a href="https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/"><code><u>IdentityTransformStream</u></code></a> provides a fast-path for pass-through transforms but is Workers-specific and implements behaviors that are not standard for a <code>TransformStream</code>. Each runtime has its own set of tricks and the natural tendency is toward non-standard solutions, because that's often the only way to make things fast.</p><p>This fragmentation hurts portability. Code that performs well on one runtime may behave differently (or poorly) on another, even though it's using "standard" APIs. The complexity burden on runtime implementers is substantial, and the subtle behavioral differences create friction for developers trying to write cross-runtime code, particularly those maintaining frameworks that must be able to run efficiently across many runtime environments.</p><p>It is also necessary to emphasize that many optimizations are only possible in parts of the spec that are unobservable to user code. The alternative, like Bun "Direct Streams", is to intentionally diverge from the spec-defined observable behaviors. This means optimizations often feel "incomplete". They work in some scenarios but not in others, in some runtimes but not others, etc. Every such case adds to the overall unsustainable complexity of the Web streams approach which is why most runtime implementers rarely put significant effort into further improvements to their streams implementations once the conformance tests are passing.</p><p>Implementers shouldn't need to jump through these hoops. When you find yourself needing to relax or bypass spec semantics just to achieve reasonable performance, that's a sign something is wrong with the spec itself. A well-designed streaming API should be efficient by default, not require each runtime to invent its own escape hatches.</p>
    <div>
      <h3>The compliance burden</h3>
      <a href="#the-compliance-burden">
        
      </a>
    </div>
    <p>A complex spec creates complex edge cases. The <a href="https://github.com/web-platform-tests/wpt/tree/master/streams"><u>Web Platform Tests for streams</u></a> span over 70 test files, and while comprehensive testing is a good thing, what's telling is what needs to be tested.</p><p>Consider some of the more obscure tests that implementations must pass:</p><ul><li><p>Prototype pollution defense: One test patches <code>Object.prototype.</code>then to intercept promise resolutions, then verifies that <code>pipeTo()</code> and <code>tee()</code> operations don't leak internal values through the prototype chain. This tests a security property that only exists because the spec's promise-heavy internals create an attack surface.</p></li><li><p>WebAssembly memory rejection: BYOB reads must explicitly reject ArrayBuffers backed by WebAssembly memory, which look like regular buffers but can't be transferred. This edge case exists because of the spec's buffer detachment model – a simpler API wouldn't need to handle it.</p></li><li><p>Crash regression for state machine conflicts: A test specifically checks that calling <code>byobRequest.respond()</code> after <code>enqueue()</code> doesn't crash the runtime. This sequence creates a conflict in the internal state machine — the <code>enqueue()</code> fulfills the pending read and should invalidate the <code>byobRequest</code>, but implementations must gracefully handle the subsequent <code>respond()</code> rather than corrupting memory in order to cover the very likely possibility that developers are not using the complex API correctly.</p></li></ul><p>These aren't contrived scenarios invented by test authors in total vacuum. They're consequences of the spec's design and reflect real world bugs.</p><p>For runtime implementers, passing the WPT suite means handling intricate corner cases that most application code will never encounter. The tests encode not just the happy path but the full matrix of interactions between readers, writers, controllers, queues, strategies, and the promise machinery that connects them all.</p><p>A simpler API would mean fewer concepts, fewer interactions between concepts, and fewer edge cases to get right resulting in more confidence that implementations actually behave consistently.</p>
    <div>
      <h3>The takeaway</h3>
      <a href="#the-takeaway">
        
      </a>
    </div>
    <p>Web streams are complex for users and implementers alike. The problems with the spec aren't bugs. They emerge from using the API exactly as designed. They aren't issues that can be fixed solely through incremental improvements. They're consequences of fundamental design choices. To improve things we need different foundations.</p>
    <div>
      <h2>A better streams API is possible</h2>
      <a href="#a-better-streams-api-is-possible">
        
      </a>
    </div>
    <p>After implementing the Web streams spec multiple times across different runtimes and seeing the pain points firsthand, I decided it was time to explore what a better, alternative streaming API could look like if designed from first principles today.</p><p>What follows is a proof of concept: it's not a finished standard, not a production-ready library, not even necessarily a concrete proposal for something new, but a starting point for discussion that demonstrates the problems with Web streams aren't inherent to streaming itself; they're consequences of specific design choices that could be made differently. Whether this exact API is the right answer is less important than whether it sparks a productive conversation about what we actually need from a streaming primitive.</p>
    <div>
      <h3>What is a stream?</h3>
      <a href="#what-is-a-stream">
        
      </a>
    </div>
    <p>Before diving into API design, it's worth asking: what is a stream?</p><p>At its core, a stream is just a sequence of data that arrives over time. You don't have all of it at once. You process it incrementally as it becomes available.</p><p>Unix pipes are perhaps the purest expression of this idea:</p>
            <pre><code>cat access.log | grep "error" | sort | uniq -c</code></pre>
            <p>
Data flows left to right. Each stage reads input, does its work, writes output. There's no pipe reader to acquire, no controller lock to manage. If a downstream stage is slow, upstream stages naturally slow down as well. Backpressure is implicit in the model, not a separate mechanism to learn (or ignore).</p><p>In JavaScript, the natural primitive for "a sequence of things that arrive over time" is already in the language: the async iterable. You consume it with <code>for await...of</code>. You stop consuming by stopping iteration.</p><p>This is the intuition the new API tries to preserve: streams should feel like iteration, because that's what they are. The complexity of Web streams – readers, writers, controllers, locks, queuing strategies – obscures this fundamental simplicity. A better API should make the simple case simple and only add complexity where it's genuinely needed.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3AUAA4bitbTOVSQg7Pd7fv/0856b44d78899dcffc4493f4146fb64f/4.png" />
          </figure>
    <div>
      <h3>Design principles</h3>
      <a href="#design-principles">
        
      </a>
    </div>
    <p>I built the proof-of-concept alternative around a different set of principles.</p>
    <div>
      <h4>Streams are iterables.</h4>
      <a href="#streams-are-iterables">
        
      </a>
    </div>
    <p>No custom <code>ReadableStream</code> class with hidden internal state. A readable stream is just an <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols"><code><u>AsyncIterable&lt;Uint8Array[]&gt;</u></code></a>. You consume it with <code>for await...of</code>. No readers to acquire, no locks to manage.</p>
    <div>
      <h4>Pull-through transforms</h4>
      <a href="#pull-through-transforms">
        
      </a>
    </div>
    <p>Transforms don't execute until the consumer pulls. There's no eager evaluation, no hidden buffering. Data flows on-demand from source, through transforms, to the consumer. If you stop iterating, processing stops.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4bEXBTEOHBMnCRKGA7odt5/cf51074cce3bb8b2ec1b5158c7560b68/5.png" />
          </figure>
    <div>
      <h4>Explicit backpressure</h4>
      <a href="#explicit-backpressure">
        
      </a>
    </div>
    <p>Backpressure is strict by default. When a buffer is full, writes reject rather than silently accumulating. You can configure alternative policies – block until space is available, drop oldest, drop newest – but you have to choose explicitly. No more silent memory growth.</p>
    <div>
      <h4>Batched chunks</h4>
      <a href="#batched-chunks">
        
      </a>
    </div>
    <p>Instead of yielding one chunk per iteration, streams yield <code>Uint8Array[]:</code> arrays of chunks. This amortizes the async overhead across multiple chunks, reducing promise creation and microtask latency in hot paths.</p>
    <div>
      <h4>Bytes only</h4>
      <a href="#bytes-only">
        
      </a>
    </div>
    <p>The API deals exclusively with bytes (<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array"><code><u>Uint8Array</u></code></a>). Strings are UTF-8 encoded automatically. There's no "value stream" vs "byte stream" dichotomy. If you want to stream arbitrary JavaScript values, use async iterables directly. While the API uses <code>Uint8Array</code>, it treats chunks as opaque. There is no partial consumption, no BYOB patterns, no byte-level operations within the streaming machinery itself. Chunks go in, chunks come out, unchanged unless a transform explicitly modifies them.</p>
    <div>
      <h4>Synchronous fast paths matter</h4>
      <a href="#synchronous-fast-paths-matter">
        
      </a>
    </div>
    <p>The API recognizes that synchronous data sources are both necessary and common. The application should not be forced to always accept the performance cost of asynchronous scheduling simply because that's the only option provided. At the same time, mixing sync and async processing can be dangerous. Synchronous paths should always be an option and should always be explicit.</p>
    <div>
      <h3>The new API in action</h3>
      <a href="#the-new-api-in-action">
        
      </a>
    </div>
    
    <div>
      <h4>Creating and consuming streams</h4>
      <a href="#creating-and-consuming-streams">
        
      </a>
    </div>
    <p>In Web streams, creating a simple producer/consumer pair requires <code>TransformStream</code>, manual encoding, and careful lock management:</p>
            <pre><code>const { readable, writable } = new TransformStream();
const enc = new TextEncoder();
const writer = writable.getWriter();
await writer.write(enc.encode("Hello, World!"));
await writer.close();
writer.releaseLock();

const dec = new TextDecoder();
let text = '';
for await (const chunk of readable) {
  text += dec.decode(chunk, { stream: true });
}
text += dec.decode();</code></pre>
            <p>Even this relatively clean version requires: a <code>TransformStream</code>, manual <code>TextEncoder</code> and <code>TextDecoder</code>, and explicit lock release.</p><p>Here's the equivalent with the new API:</p>
            <pre><code>import { Stream } from 'new-streams';

// Create a push stream
const { writer, readable } = Stream.push();

// Write data — backpressure is enforced
await writer.write("Hello, World!");
await writer.end();

// Consume as text
const text = await Stream.text(readable);</code></pre>
            <p>The readable is just an async iterable. You can pass it to any function that expects one, including <code>Stream.text()</code> which collects and decodes the entire stream.</p><p>The writer has a simple interface: <code>write(), writev()</code> for batched writes, <code>end()</code> to signal completion, and <code>abort()</code> for errors. That's essentially it.</p><p>The Writer is not a concrete class. Any object that implements <code>write()</code>, <code>end()</code>, and <code>abort()</code> can be a writer making it easy to adapt existing APIs or create specialized implementations without subclassing. There's no complex <code>UnderlyingSink</code> protocol with <code>start()</code>, <code>write()</code>, <code>close()</code>, <code>and abort() </code>callbacks that must coordinate through a controller whose lifecycle and state are independent of the <code>WritableStream</code> it is bound to.</p><p>Here's a simple in-memory writer that collects all written data:</p>
            <pre><code>// A minimal writer implementation — just an object with methods
function createBufferWriter() {
  const chunks = [];
  let totalBytes = 0;
  let closed = false;

  const addChunk = (chunk) =&gt; {
    chunks.push(chunk);
    totalBytes += chunk.byteLength;
  };

  return {
    get desiredSize() { return closed ? null : 1; },

    // Async variants
    write(chunk) { addChunk(chunk); },
    writev(batch) { for (const c of batch) addChunk(c); },
    end() { closed = true; return totalBytes; },
    abort(reason) { closed = true; chunks.length = 0; },

    // Sync variants return boolean (true = accepted)
    writeSync(chunk) { addChunk(chunk); return true; },
    writevSync(batch) { for (const c of batch) addChunk(c); return true; },
    endSync() { closed = true; return totalBytes; },
    abortSync(reason) { closed = true; chunks.length = 0; return true; },

    getChunks() { return chunks; }
  };
}

// Use it
const writer = createBufferWriter();
await Stream.pipeTo(source, writer);
const allData = writer.getChunks();</code></pre>
            <p>No base class to extend, no abstract methods to implement, no controller to coordinate with. Just an object with the right shape.</p>
    <div>
      <h4>Pull-through transforms</h4>
      <a href="#pull-through-transforms">
        
      </a>
    </div>
    <p>Under the new API design, transforms should not perform any work until the data is being consumed. This is a fundamental principle.</p>
            <pre><code>// Nothing executes until iteration begins
const output = Stream.pull(source, compress, encrypt);

// Transforms execute as we iterate
for await (const chunks of output) {
  for (const chunk of chunks) {
    process(chunk);
  }
}</code></pre>
            <p><code>Stream.pull()</code> creates a lazy pipeline. The <code>compress</code> and <code>encrypt</code> transforms don't run until you start iterating output. Each iteration pulls data through the pipeline on demand.</p><p>This is fundamentally different from Web streams' <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/pipeThrough"><code><u>pipeThrough()</u></code></a>, which starts actively pumping data from the source to the transform as soon as you set up the pipe. Pull semantics mean you control when processing happens, and stopping iteration stops processing.</p><p>Transforms can be stateless or stateful. A stateless transform is just a function that takes chunks and returns transformed chunks:</p>
            <pre><code>// Stateless transform — a pure function
// Receives chunks or null (flush signal)
const toUpperCase = (chunks) =&gt; {
  if (chunks === null) return null; // End of stream
  return chunks.map(chunk =&gt; {
    const str = new TextDecoder().decode(chunk);
    return new TextEncoder().encode(str.toUpperCase());
  });
};

// Use it directly
const output = Stream.pull(source, toUpperCase);</code></pre>
            <p>Stateful transforms are simple objects with member functions that maintain state across calls:</p>
            <pre><code>// Stateful transform — a generator that wraps the source
function createLineParser() {
  // Helper to concatenate Uint8Arrays
  const concat = (...arrays) =&gt; {
    const result = new Uint8Array(arrays.reduce((n, a) =&gt; n + a.length, 0));
    let offset = 0;
    for (const arr of arrays) { result.set(arr, offset); offset += arr.length; }
    return result;
  };

  return {
    async *transform(source) {
      let pending = new Uint8Array(0);
      
      for await (const chunks of source) {
        if (chunks === null) {
          // Flush: yield any remaining data
          if (pending.length &gt; 0) yield [pending];
          continue;
        }
        
        // Concatenate pending data with new chunks
        const combined = concat(pending, ...chunks);
        const lines = [];
        let start = 0;

        for (let i = 0; i &lt; combined.length; i++) {
          if (combined[i] === 0x0a) { // newline
            lines.push(combined.slice(start, i));
            start = i + 1;
          }
        }

        pending = combined.slice(start);
        if (lines.length &gt; 0) yield lines;
      }
    }
  };
}

const output = Stream.pull(source, createLineParser());</code></pre>
            <p>For transforms that need cleanup on abort, add an abort handler:</p>
            <pre><code>// Stateful transform with resource cleanup
function createGzipCompressor() {
  // Hypothetical compression API...
  const deflate = new Deflater({ gzip: true });

  return {
    async *transform(source) {
      for await (const chunks of source) {
        if (chunks === null) {
          // Flush: finalize compression
          deflate.push(new Uint8Array(0), true);
          if (deflate.result) yield [deflate.result];
        } else {
          for (const chunk of chunks) {
            deflate.push(chunk, false);
            if (deflate.result) yield [deflate.result];
          }
        }
      }
    },
    abort(reason) {
      // Clean up compressor resources on error/cancellation
    }
  };
}</code></pre>
            <p>For implementers, there's no Transformer protocol with <code>start()</code>, <code>transform()</code>, <code>flush()</code> methods and controller coordination passed into a <code>TransformStream</code> class that has its own hidden state machine and buffering mechanisms. Transforms are just functions or simple objects: far simpler to implement and test.</p>
    <div>
      <h4>Explicit backpressure policies</h4>
      <a href="#explicit-backpressure-policies">
        
      </a>
    </div>
    <p>When a bounded buffer fills up and a producer wants to write more, there are only a few things you can do:</p><ol><li><p>Reject the write: refuse to accept more data</p></li><li><p>Wait: block until space becomes available</p></li><li><p>Discard old data: evict what's already buffered to make room</p></li><li><p>Discard new data: drop what's incoming</p></li></ol><p>That's it. Any other response is either a variation of these (like "resize the buffer," which is really just deferring the choice) or domain-specific logic that doesn't belong in a general streaming primitive. Web streams currently always choose Wait by default.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/68339c8QsvNmb7JcZ2lSDO/e52a86a9b8f52b52eb9328d5ee58f23a/6.png" />
          </figure><p>The new API makes you choose one of these four explicitly:</p><ul><li><p><code>strict</code> (default): Rejects writes when the buffer is full and too many writes are pending. Catches "fire-and-forget" patterns where producers ignore backpressure.</p></li><li><p><code>block</code>: Writes wait until buffer space is available. Use when you trust the producer to await writes properly.</p></li><li><p><code>drop-oldest</code>: Drops the oldest buffered data to make room. Useful for live feeds where stale data loses value.</p></li><li><p><code>drop-newest</code>: Discards incoming data when full. Useful when you want to process what you have without being overwhelmed.</p></li></ul>
            <pre><code>const { writer, readable } = Stream.push({
  highWaterMark: 10,
  backpressure: 'strict' // or 'block', 'drop-oldest', 'drop-newest'
});</code></pre>
            <p>No more hoping producers cooperate. The policy you choose determines what happens when the buffer fills.</p><p>Here's how each policy behaves when a producer writes faster than the consumer reads:</p>
            <pre><code>// strict: Catches fire-and-forget writes that ignore backpressure
const strict = Stream.push({ highWaterMark: 2, backpressure: 'strict' });
strict.writer.write(chunk1);  // ok (not awaited)
strict.writer.write(chunk2);  // ok (fills slots buffer)
strict.writer.write(chunk3);  // ok (queued in pending)
strict.writer.write(chunk4);  // ok (pending buffer fills)
strict.writer.write(chunk5);  // throws! too many pending writes

// block: Wait for space (unbounded pending queue)
const blocking = Stream.push({ highWaterMark: 2, backpressure: 'block' });
await blocking.writer.write(chunk1);  // ok
await blocking.writer.write(chunk2);  // ok
await blocking.writer.write(chunk3);  // waits until consumer reads
await blocking.writer.write(chunk4);  // waits until consumer reads
await blocking.writer.write(chunk5);  // waits until consumer reads

// drop-oldest: Discard old data to make room
const dropOld = Stream.push({ highWaterMark: 2, backpressure: 'drop-oldest' });
await dropOld.writer.write(chunk1);  // ok
await dropOld.writer.write(chunk2);  // ok
await dropOld.writer.write(chunk3);  // ok, chunk1 discarded

// drop-newest: Discard incoming data when full
const dropNew = Stream.push({ highWaterMark: 2, backpressure: 'drop-newest' });
await dropNew.writer.write(chunk1);  // ok
await dropNew.writer.write(chunk2);  // ok
await dropNew.writer.write(chunk3);  // silently dropped</code></pre>
            
    <div>
      <h4>Explicit Multi-consumer patterns</h4>
      <a href="#explicit-multi-consumer-patterns">
        
      </a>
    </div>
    
            <pre><code>// Share with explicit buffer management
const shared = Stream.share(source, {
  highWaterMark: 100,
  backpressure: 'strict'
});

const consumer1 = shared.pull();
const consumer2 = shared.pull(decompress);</code></pre>
            <p>Instead of <code>tee()</code> with its hidden unbounded buffer, you get explicit multi-consumer primitives. <code>Stream.share()</code> is pull-based: consumers pull from a shared source, and you configure the buffer limits and backpressure policy upfront.</p><p>There's also <code>Stream.broadcast()</code> for push-based multi-consumer scenarios. Both require you to think about what happens when consumers run at different speeds, because that's a real concern that shouldn't be hidden.</p>
    <div>
      <h4>Sync/async separation</h4>
      <a href="#sync-async-separation">
        
      </a>
    </div>
    <p>Not all streaming workloads involve I/O. When your source is in-memory and your transforms are pure functions, async machinery adds overhead without benefit. You're paying for coordination of "waiting" that adds no benefit.</p><p>The new API has complete parallel sync versions: <code>Stream.pullSync()</code>, <code>Stream.bytesSync()</code>, <code>Stream.textSync()</code>, and so on. If your source and transforms are all synchronous, you can process the entire pipeline without a single promise.</p>
            <pre><code>// Async — when source or transforms may be asynchronous
const textAsync = await Stream.text(source);

// Sync — when all components are synchronous
const textSync = Stream.textSync(source);</code></pre>
            <p>Here's a complete synchronous pipeline – compression, transformation, and consumption with zero async overhead:</p>
            <pre><code>// Synchronous source from in-memory data
const source = Stream.fromSync([inputBuffer]);

// Synchronous transforms
const compressed = Stream.pullSync(source, zlibCompressSync);
const encrypted = Stream.pullSync(compressed, aesEncryptSync);

// Synchronous consumption — no promises, no event loop trips
const result = Stream.bytesSync(encrypted);</code></pre>
            <p>The entire pipeline executes in a single call stack. No promises are created, no microtask queue scheduling occurs, and no GC pressure from short-lived async machinery. For CPU-bound workloads like parsing, compression, or transformation of in-memory data, this can be significantly faster than the equivalent Web streams code – which would force async boundaries even when every component is synchronous.</p><p>Web streams has no synchronous path. Even if your source has data ready and your transform is a pure function, you still pay for promise creation and microtask scheduling on every operation. Promises are fantastic for cases in which waiting is actually necessary, but they aren't always necessary. The new API lets you stay in sync-land when that's what you need.</p>
    <div>
      <h4>Bridging the gap between this and web streams</h4>
      <a href="#bridging-the-gap-between-this-and-web-streams">
        
      </a>
    </div>
    <p>The async iterator based approach provides a natural bridge between this alternative approach and Web streams. When coming from a ReadableStream to this new approach, simply passing the readable in as input works as expected when the ReadableStream is set up to yield bytes:</p>
            <pre><code>const readable = getWebReadableStreamSomehow();
const input = Stream.pull(readable, transform1, transform2);
for await (const chunks of input) {
  // process chunks
}</code></pre>
            <p>When adapting to a ReadableStream, a bit more work is required since the alternative approach yields batches of chunks, but the adaptation layer is as easily straightforward:</p>
            <pre><code>async function* adapt(input) {
  for await (const chunks of input) {
    for (const chunk of chunks) {
      yield chunk;
    }
  }
}

const input = Stream.pull(source, transform1, transform2);
const readable = ReadableStream.from(adapt(input));</code></pre>
            
    <div>
      <h4>How this addresses the real-world failures from earlier</h4>
      <a href="#how-this-addresses-the-real-world-failures-from-earlier">
        
      </a>
    </div>
    <ul><li><p>Unconsumed bodies: Pull semantics mean nothing happens until you iterate. No hidden resource retention. If you don't consume a stream, there's no background machinery holding connections open.</p></li><li><p>The <code>tee()</code> memory cliff: <code>Stream.share()</code> requires explicit buffer configuration. You choose the <code>highWaterMark</code> and backpressure policy upfront: no more silent unbounded growth when consumers run at different speeds.</p></li><li><p>Transform backpressure gaps: Pull-through transforms execute on-demand. Data doesn't cascade through intermediate buffers; it flows only when the consumer pulls. Stop iterating, stop processing.</p></li><li><p>GC thrashing in SSR: Batched chunks (<code>Uint8Array[]</code>) amortize async overhead. Sync pipelines via <code>Stream.pullSync()</code> eliminate promise allocation entirely for CPU-bound workloads.</p></li></ul>
    <div>
      <h3>Performance</h3>
      <a href="#performance">
        
      </a>
    </div>
    <p>The design choices have performance implications. Here are benchmarks from the reference implementation of this possible alternative compared to Web streams (Node.js v24.x, Apple M1 Pro, averaged over 10 runs):</p><table><tr><td><p><b>Scenario</b></p></td><td><p><b>Alternative</b></p></td><td><p><b>Web streams</b></p></td><td><p><b>Difference</b></p></td></tr><tr><td><p>Small chunks (1KB × 5000)</p></td><td><p>~13 GB/s</p></td><td><p>~4 GB/s</p></td><td><p>~3× faster</p></td></tr><tr><td><p>Tiny chunks (100B × 10000)</p></td><td><p>~4 GB/s</p></td><td><p>~450 MB/s</p></td><td><p>~8× faster</p></td></tr><tr><td><p>Async iteration (8KB × 1000)</p></td><td><p>~530 GB/s</p></td><td><p>~35 GB/s</p></td><td><p>~15× faster</p></td></tr><tr><td><p>Chained 3× transforms (8KB × 500)</p></td><td><p>~275 GB/s</p></td><td><p>~3 GB/s</p></td><td><p><b>~80–90× faster</b></p></td></tr><tr><td><p>High-frequency (64B × 20000)</p></td><td><p>~7.5 GB/s</p></td><td><p>~280 MB/s</p></td><td><p>~25× faster</p></td></tr></table><p>The chained transform result is particularly striking: pull-through semantics eliminate the intermediate buffering that plagues Web streams pipelines. Instead of each <code>TransformStream</code> eagerly filling its internal buffers, data flows on-demand from consumer to source.</p><p>Now, to be fair, Node.js really has not yet put significant effort into fully optimizing the performance of its Web streams implementation. There's likely significant room for improvement in Node.js' performance results through a bit of applied effort to optimize the hot paths there. That said, running these benchmarks in Deno and Bun also show a significant performance improvement with this alternative iterator based approach than in either of their Web streams implementations as well.</p><p>Browser benchmarks (Chrome/Blink, averaged over 3 runs) show consistent gains as well:</p><table><tr><td><p><b>Scenario</b></p></td><td><p><b>Alternative</b></p></td><td><p><b>Web streams</b></p></td><td><p><b>Difference</b></p></td></tr><tr><td><p>Push 3KB chunks</p></td><td><p>~135k ops/s</p></td><td><p>~24k ops/s</p></td><td><p>~5–6× faster</p></td></tr><tr><td><p>Push 100KB chunks</p></td><td><p>~24k ops/s</p></td><td><p>~3k ops/s</p></td><td><p>~7–8× faster</p></td></tr><tr><td><p>3 transform chain</p></td><td><p>~4.6k ops/s</p></td><td><p>~880 ops/s</p></td><td><p>~5× faster</p></td></tr><tr><td><p>5 transform chain</p></td><td><p>~2.4k ops/s</p></td><td><p>~550 ops/s</p></td><td><p>~4× faster</p></td></tr><tr><td><p>bytes() consumption</p></td><td><p>~73k ops/s</p></td><td><p>~11k ops/s</p></td><td><p>~6–7× faster</p></td></tr><tr><td><p>Async iteration</p></td><td><p>~1.1M ops/s</p></td><td><p>~10k ops/s</p></td><td><p><b>~40–100× faster</b></p></td></tr></table><p>These benchmarks measure throughput in controlled scenarios; real-world performance depends on your specific use case. The difference between Node.js and browser gains reflects the distinct optimization paths each environment takes for Web streams.</p><p>It's worth noting that these benchmarks compare a pure TypeScript/JavaScript implementation of the new API against the native (JavaScript/C++/Rust) implementations of Web streams in each runtime. The new API's reference implementation has had no performance optimization work; the gains come entirely from the design. A native implementation would likely show further improvement.</p><p>The gains illustrate how fundamental design choices compound: batching amortizes async overhead, pull semantics eliminate intermediate buffering, and the freedom for implementations to use synchronous fast paths when data is available immediately all contribute.</p><blockquote><p>"We’ve done a lot to improve performance and consistency in Node streams, but there’s something uniquely powerful about starting from scratch. New streams’ approach embraces modern runtime realities without legacy baggage, and that opens the door to a simpler, performant and more coherent streams model." 
- Robert Nagy, Node.js TSC member and Node.js streams contributor</p></blockquote>
    <div>
      <h2>What's next</h2>
      <a href="#whats-next">
        
      </a>
    </div>
    <p>I'm publishing this to start a conversation. What did I get right? What did I miss? Are there use cases that don't fit this model? What would a migration path for this approach look like? The goal is to gather feedback from developers who've felt the pain of Web streams and have opinions about what a better API should look like.</p>
    <div>
      <h3>Try it yourself</h3>
      <a href="#try-it-yourself">
        
      </a>
    </div>
    <p>A reference implementation for this alternative approach is available now and can be found at <a href="https://github.com/jasnell/new-streams"><u>https://github.com/jasnell/new-streams</u></a>.</p><ul><li><p>API Reference: See the <a href="https://github.com/jasnell/new-streams/blob/main/API.md"><u>API.md</u></a> for complete documentation</p></li><li><p>Examples: The <a href="https://github.com/jasnell/new-streams/tree/main/samples"><u>samples directory</u></a> has working code for common patterns</p></li></ul><p>I welcome issues, discussions, and pull requests. If you've run into Web streams problems I haven't covered, or if you see gaps in this approach, let me know. But again, the idea here is not to say "Let's all use this shiny new object!"; it is to kick off a discussion that looks beyond the current status quo of Web Streams and returns back to first principles.</p><p>Web streams was an ambitious project that brought streaming to the web platform when nothing else existed. The people who designed it made reasonable choices given the constraints of 2014 – before async iteration, before years of production experience revealed the edge cases.</p><p>But we've learned a lot since then. JavaScript has evolved. A streaming API designed today can be simpler, more aligned with the language, and more explicit about the things that matter, like backpressure and multi-consumer behavior.</p><p>We deserve a better stream API. So let's talk about what that could look like.</p> ]]></content:encoded>
            <category><![CDATA[Standards]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[TypeScript]]></category>
            <category><![CDATA[Open Source]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Node.js]]></category>
            <category><![CDATA[Performance]]></category>
            <category><![CDATA[API]]></category>
            <guid isPermaLink="false">37h1uszA2vuOfmXb3oAnZr</guid>
            <dc:creator>James M Snell</dc:creator>
        </item>
        <item>
            <title><![CDATA[What came first: the CNAME or the A record?]]></title>
            <link>https://blog.cloudflare.com/cname-a-record-order-dns-standards/</link>
            <pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[ A recent change to 1.1.1.1 accidentally altered the order of CNAME records in DNS responses, breaking resolution for some clients. This post explores the technical root cause, examines the source code of affected resolvers, and dives into the inherent ambiguities of the DNS RFCs.   ]]></description>
            <content:encoded><![CDATA[ <p>On January 8, 2026, a routine update to 1.1.1.1 aimed at reducing memory usage accidentally triggered a wave of DNS resolution failures for users across the Internet. The root cause wasn't an attack or an outage, but a subtle shift in the order of records within our DNS responses.  </p><p>While most modern software treats the order of records in DNS responses as irrelevant, we discovered that some implementations expect CNAME records to appear before everything else. When that order changed, resolution started failing. This post explores the code change that caused the shift, why it broke specific DNS clients, and the 40-year-old protocol ambiguity that makes the "correct" order of a DNS response difficult to define.</p>
    <div>
      <h2>Timeline</h2>
      <a href="#timeline">
        
      </a>
    </div>
    <p><i>All timestamps referenced are in Coordinated Universal Time (UTC).</i></p><table><tr><th><p><b>Time</b></p></th><th><p><b>Description</b></p></th></tr><tr><td><p>2025-12-02</p></td><td><p>The record reordering is introduced to the 1.1.1.1 codebase</p></td></tr><tr><td><p>2025-12-10</p></td><td><p>The change is released to our testing environment</p></td></tr><tr><td><p>2026-01-07 23:48</p></td><td><p>A global release containing the change starts</p></td></tr><tr><td><p>2026-01-08 17:40</p></td><td><p>The release reaches 90% of servers</p></td></tr><tr><td><p>2026-01-08 18:19</p></td><td><p>Incident is declared</p></td></tr><tr><td><p>2026-01-08 18:27</p></td><td><p>The release is reverted</p></td></tr><tr><td><p>2026-01-08 19:55</p></td><td><p>Revert is completed. Impact ends</p></td></tr></table>
    <div>
      <h2>What happened?</h2>
      <a href="#what-happened">
        
      </a>
    </div>
    <p>While making some improvements to lower the memory usage of our cache implementation, we introduced a subtle change to CNAME record ordering. The change was introduced on December 2, 2025, released to our testing environment on December 10, and began deployment on January 7, 2026.</p>
    <div>
      <h3>How DNS CNAME chains work</h3>
      <a href="#how-dns-cname-chains-work">
        
      </a>
    </div>
    <p>When you query for a domain like <code>www.example.com</code>, you might get a <a href="https://www.cloudflare.com/learning/dns/dns-records/dns-cname-record/"><u>CNAME (Canonical Name)</u></a> record that indicates one name is an alias for another name. It’s the job of public resolvers, such as <a href="https://www.cloudflare.com/learning/dns/what-is-1.1.1.1/"><u>1.1.1.1</u></a>, to follow this chain of aliases until it reaches a final response:</p><p><code>www.example.com → cdn.example.com → server.cdn-provider.com → 198.51.100.1</code></p><p>As 1.1.1.1 traverses this chain, it caches every intermediate record. Each record in the chain has its own <a href="https://www.cloudflare.com/learning/cdn/glossary/time-to-live-ttl/"><u>TTL (Time-To-Live)</u></a>, indicating how long we can cache it. Not all the TTLs in a CNAME chain need to be the same:</p><p><code>www.example.com → cdn.example.com (TTL: 3600 seconds) # Still cached
cdn.example.com → 198.51.100.1    (TTL: 300 seconds)  # Expired</code></p><p>When one or more records in a CNAME chain expire, it’s considered partially expired. Fortunately, since parts of the chain are still in our cache, we don’t have to resolve the entire CNAME chain again — only the part that has expired. In our example above, we would take the still valid <code>www.example.com → cdn.example.com</code> chain, and only resolve the expired <code>cdn.example.com</code> <a href="https://www.cloudflare.com/learning/dns/dns-records/dns-a-record/"><u>A record</u></a>. Once that’s done, we combine the existing CNAME chain and the newly resolved records into a single response.</p>
    <div>
      <h3>The logic change</h3>
      <a href="#the-logic-change">
        
      </a>
    </div>
    <p>The code that merges these two chains is where the change occurred. Previously, the code would create a new list, insert the existing CNAME chain, and then append the new records:</p>
            <pre><code>impl PartialChain {
    /// Merges records to the cache entry to make the cached records complete.
    pub fn fill_cache(&amp;self, entry: &amp;mut CacheEntry) {
        let mut answer_rrs = Vec::with_capacity(entry.answer.len() + self.records.len());
        answer_rrs.extend_from_slice(&amp;self.records); // CNAMEs first
        answer_rrs.extend_from_slice(&amp;entry.answer); // Then A/AAAA records
        entry.answer = answer_rrs;
    }
}
</code></pre>
            <p>However, to save some memory allocations and copies, the code was changed to instead append the CNAMEs to the existing answer list:</p>
            <pre><code>impl PartialChain {
    /// Merges records to the cache entry to make the cached records complete.
    pub fn fill_cache(&amp;self, entry: &amp;mut CacheEntry) {
        entry.answer.extend(self.records); // CNAMEs last
    }
}
</code></pre>
            <p>As a result, the responses that 1.1.1.1 returned now sometimes had the CNAME records appearing at the bottom, after the final resolved answer.</p>
    <div>
      <h3>Why this caused impact</h3>
      <a href="#why-this-caused-impact">
        
      </a>
    </div>
    <p>When DNS clients receive a response with a CNAME chain in the answer section, they also need to follow this chain to find out that <code>www.example.com</code> points to <code>198.51.100.1</code>. Some DNS client implementations handle this by keeping track of the expected name for the records as they’re iterated sequentially. When a CNAME is encountered, the expected name is updated:</p>
            <pre><code>;; QUESTION SECTION:
;; www.example.com.        IN    A

;; ANSWER SECTION:
www.example.com.    3600   IN    CNAME  cdn.example.com.
cdn.example.com.    300    IN    A      198.51.100.1
</code></pre>
            <p></p><ol><li><p>Find records for <code>www.example.com</code></p></li><li><p>Encounter <code>www.example.com. CNAME cdn.example.com</code></p></li><li><p>Find records for <code>cdn.example.com</code></p></li><li><p>Encounter <code>cdn.example.com. A 198.51.100.1</code></p></li></ol><p>When the CNAME suddenly appears at the bottom, this no longer works:</p>
            <pre><code>;; QUESTION SECTION:
;; www.example.com.	       IN    A

;; ANSWER SECTION:
cdn.example.com.    300    IN    A      198.51.100.1
www.example.com.    3600   IN    CNAME  cdn.example.com.
</code></pre>
            <p></p><ol><li><p>Find records for <code>www.example.com</code></p></li><li><p>Ignore <code>cdn.example.com. A 198.51.100.1</code> as it doesn’t match the expected name</p></li><li><p>Encounter <code>www.example.com. CNAME cdn.example.com</code></p></li><li><p>Find records for <code>cdn.example.com</code></p></li><li><p>No more records are present, so the response is considered empty</p></li></ol><p>One such implementation that broke is the <a href="https://man7.org/linux/man-pages/man3/getaddrinfo.3.html"><code><u>getaddrinfo</u></code></a> function in glibc, which is commonly used on Linux for DNS resolution. When looking at its <code>getanswer_r</code> implementation, we can indeed see it expects to find the CNAME records before any answers:</p>
            <pre><code>for (; ancount &gt; 0; --ancount)
  {
    // ... parsing DNS records ...
    
    if (rr.rtype == T_CNAME)
      {
        /* Record the CNAME target as the new expected name. */
        int n = __ns_name_unpack (c.begin, c.end, rr.rdata,
                                  name_buffer, sizeof (name_buffer));
        expected_name = name_buffer;  // Update what we're looking for
      }
    else if (rr.rtype == qtype
             &amp;&amp; __ns_samebinaryname (rr.rname, expected_name)  // Must match!
             &amp;&amp; rr.rdlength == rrtype_to_rdata_length (type:qtype))
      {
        /* Address record matches - store it */
        ptrlist_add (list:addresses, item:(char *) alloc_buffer_next (abuf, uint32_t));
        alloc_buffer_copy_bytes (buf:abuf, src:rr.rdata, size:rr.rdlength);
      }
  }
</code></pre>
            <p>Another notable affected implementation was the DNSC process in three models of Cisco ethernet switches. In the case where switches had been configured to use 1.1.1.1 these switches experienced spontaneous reboot loops when they received a response containing the reordered CNAMEs. <a href="https://www.cisco.com/c/en/us/support/docs/smb/switches/Catalyst-switches/kmgmt3846-cbs-reboot-with-fatal-error-from-dnsc-process.html"><u>Cisco has published a service document describing the issue</u></a>.</p>
    <div>
      <h3>Not all implementations break</h3>
      <a href="#not-all-implementations-break">
        
      </a>
    </div>
    <p>Most DNS clients don’t have this issue. For example, <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-resolved.service.html"><u>systemd-resolved</u></a> first parses the records into an ordered set:</p>
            <pre><code>typedef struct DnsAnswerItem {
        DnsResourceRecord *rr; // The actual record
        DnsAnswerFlags flags;  // Which section it came from
        // ... other metadata
} DnsAnswerItem;


typedef struct DnsAnswer {
        unsigned n_ref;
        OrderedSet *items;
} DnsAnswer;
</code></pre>
            <p>When following a CNAME chain it can then search the entire answer set, even if the CNAME records don’t appear at the top.</p>
    <div>
      <h2>What the RFC says</h2>
      <a href="#what-the-rfc-says">
        
      </a>
    </div>
    <p><a href="https://datatracker.ietf.org/doc/html/rfc1034"><u>RFC 1034</u></a>, published in 1987, defines much of the behavior of the DNS protocol, and should give us an answer on whether the order of CNAME records matters. <a href="https://datatracker.ietf.org/doc/html/rfc1034#section-4.3.1"><u>Section 4.3.1</u></a> contains the following text:</p><blockquote><p>If recursive service is requested and available, the recursive response to a query will be one of the following:</p><p>- The answer to the query, possibly preface by one or more CNAME RRs that specify aliases encountered on the way to an answer.</p></blockquote><p>While "possibly preface" can be interpreted as a requirement for CNAME records to appear before everything else, it does not use normative key words, such as <a href="https://datatracker.ietf.org/doc/html/rfc2119"><u>MUST and SHOULD</u></a> that modern RFCs use to express requirements. This isn’t a flaw in RFC 1034, but simply a result of its age. <a href="https://datatracker.ietf.org/doc/html/rfc2119"><u>RFC 2119</u></a>, which standardized these key words, was published in 1997, 10 years <i>after</i> RFC 1034.</p><p>In our case, we did originally implement the specification so that CNAMEs appear first. However, we did not have any tests asserting the behavior remains consistent due to the ambiguous language in the RFC.</p>
    <div>
      <h3>The subtle distinction: RRsets vs RRs in message sections</h3>
      <a href="#the-subtle-distinction-rrsets-vs-rrs-in-message-sections">
        
      </a>
    </div>
    <p>To understand why this ambiguity exists, we need to understand a subtle but important distinction in DNS terminology.</p><p>RFC 1034 <a href="https://datatracker.ietf.org/doc/html/rfc1034#section-3.6"><u>section 3.6</u></a> defines Resource Record Sets (RRsets) as collections of records with the same name, type, and class. For RRsets, the specification is clear about ordering:</p><blockquote><p>The order of RRs in a set is not significant, and need not be preserved by name servers, resolvers, or other parts of the DNS.</p></blockquote><p>However, RFC 1034 doesn’t clearly specify how message sections relate to RRsets. While modern DNS specifications have shown that message sections can indeed contain multiple RRsets (consider <a href="https://www.cloudflare.com/learning/dns/dnssec/how-dnssec-works/">DNSSEC</a> responses with signatures), RFC 1034 doesn’t describe message sections in those terms. Instead, it treats message sections as containing individual Resource Records (RRs).</p><p>The problem is that the RFC primarily discusses ordering in the context of RRsets but doesn't specify the ordering of different RRsets relative to each other within a message section. This is where the ambiguity lives.</p><p>RFC 1034 <a href="https://datatracker.ietf.org/doc/html/rfc1034#section-6.2.1"><u>section 6.2.1</u></a> includes an example that demonstrates this ambiguity further. It mentions that the order of Resource Records (RRs) is not significant either:</p><blockquote><p>The difference in ordering of the RRs in the answer section is not significant.</p></blockquote><p>However, this example only shows two A records for the same name within the same RRset. It doesn't address whether this applies to different record types like CNAMEs and A records.</p>
    <div>
      <h2>CNAME chain ordering</h2>
      <a href="#cname-chain-ordering">
        
      </a>
    </div>
    <p>It turns out that this issue extends beyond putting CNAME records before other record types. Even when CNAMEs appear before other records, sequential parsing can still break if the CNAME chain itself is out of order. Consider the following response:</p>
            <pre><code>;; QUESTION SECTION:
;; www.example.com.              IN    A

;; ANSWER SECTION:
cdn.example.com.           3600  IN    CNAME  server.cdn-provider.com.
www.example.com.           3600  IN    CNAME  cdn.example.com.
server.cdn-provider.com.   300   IN    A      198.51.100.1
</code></pre>
            <p>Each CNAME belongs to a different RRset, as they have different owners, so the statement about RRset order being insignificant doesn’t apply here.</p><p>However, RFC 1034 doesn't specify that CNAME chains must appear in any particular order. There's no requirement that <code>www.example.com. CNAME cdn.example.com.</code> must appear before <code>cdn.example.com. CNAME server.cdn-provider.com.</code>. With sequential parsing, the same issue occurs:</p><ol><li><p>Find records for <code>www.example.com</code></p></li><li><p>Ignore <code>cdn.example.com. CNAME server.cdn-provider.com</code>. as it doesn’t match the expected name</p></li><li><p>Encounter <code>www.example.com. CNAME cdn.example.com</code></p></li><li><p>Find records for <code>cdn.example.com</code></p></li><li><p>Ignore <code>server.cdn-provider.com. A 198.51.100.1</code> as it doesn’t match the expected name</p></li></ol>
    <div>
      <h2>What should resolvers do?</h2>
      <a href="#what-should-resolvers-do">
        
      </a>
    </div>
    <p>RFC 1034 section 5 describes resolver behavior. <a href="https://datatracker.ietf.org/doc/html/rfc1034#section-5.2.2"><u>Section 5.2.2</u></a> specifically addresses how resolvers should handle aliases (CNAMEs): </p><blockquote><p>In most cases a resolver simply restarts the query at the new name when it encounters a CNAME.</p></blockquote><p>This suggests that resolvers should restart the query upon finding a CNAME, regardless of where it appears in the response. However, it's important to distinguish between different types of resolvers:</p><ul><li><p>Recursive resolvers, like 1.1.1.1, are full DNS resolvers that perform recursive resolution by querying authoritative nameservers</p></li><li><p>Stub resolvers, like glibc’s getaddrinfo, are simplified local interfaces that forward queries to recursive resolvers and process the responses</p></li></ul><p>The RFC sections on resolver behavior were primarily written with full resolvers in mind, not the simplified stub resolvers that most applications actually use. Some stub resolvers evidently don’t implement certain parts of the spec, such as the CNAME-restart logic described in the RFC. </p>
    <div>
      <h2>The DNSSEC specifications provide contrast</h2>
      <a href="#the-dnssec-specifications-provide-contrast">
        
      </a>
    </div>
    <p>Later DNS specifications demonstrate a different approach to defining record ordering. <a href="https://datatracker.ietf.org/doc/html/rfc4035"><u>RFC 4035</u></a>, which defines protocol modifications for <a href="https://www.cloudflare.com/learning/dns/dnssec/how-dnssec-works/"><u>DNSSEC</u></a>, uses more explicit language:</p><blockquote><p>When placing a signed RRset in the Answer section, the name server MUST also place its RRSIG RRs in the Answer section. The RRSIG RRs have a higher priority for inclusion than any other RRsets that may have to be included.</p></blockquote><p>The specification uses "MUST" and explicitly defines "higher priority" for <a href="https://www.cloudflare.com/learning/dns/dnssec/how-dnssec-works/"><u>RRSIG</u></a> records. However, "higher priority for inclusion" refers to whether RRSIGs should be included in the response, not where they should appear. This provides unambiguous guidance to implementers about record inclusion in DNSSEC contexts, while not mandating any particular behavior around record ordering.</p><p>For unsigned zones, however, the ambiguity from RFC 1034 remains. The word "preface" has guided implementation behavior for nearly four decades, but it has never been formally specified as a requirement.</p>
    <div>
      <h2>Do CNAME records come first?</h2>
      <a href="#do-cname-records-come-first">
        
      </a>
    </div>
    <p>While in our interpretation the RFCs do not require CNAMEs to appear in any particular order, it’s clear that at least some widely-deployed DNS clients rely on it. As some systems using these clients might be updated infrequently, or never updated at all, we believe it’s best to require CNAME records to appear in-order before any other records.</p><p>Based on what we have learned during this incident, we have reverted the CNAME re-ordering and do not intend to change the order in the future.</p><p>To prevent any future incidents or confusion, we have written a proposal in the form of an <a href="https://www.ietf.org/participate/ids/"><u>Internet-Draft</u></a> to be discussed at the IETF. If consensus is reached on the clarified behavior, this would become an RFC that explicitly defines how to correctly handle CNAMEs in DNS responses, helping us and the wider DNS community navigate the protocol. The proposal can be found at <a href="https://datatracker.ietf.org/doc/draft-jabley-dnsop-ordered-answer-section/">https://datatracker.ietf.org/doc/draft-jabley-dnsop-ordered-answer-section</a>. If you have suggestions or feedback we would love to hear your opinions, most usefully via the <a href="https://datatracker.ietf.org/wg/dnsop/about/"><u>DNSOP working group</u></a> at the IETF.</p> ]]></content:encoded>
            <category><![CDATA[1.1.1.1]]></category>
            <category><![CDATA[Post Mortem]]></category>
            <category><![CDATA[DNS]]></category>
            <category><![CDATA[Resolver]]></category>
            <category><![CDATA[Standards]]></category>
            <category><![CDATA[Bugs]]></category>
            <category><![CDATA[Consumer Services]]></category>
            <guid isPermaLink="false">3fP84BsxwSxKr7ffpmVO6s</guid>
            <dc:creator>Sebastiaan Neuteboom</dc:creator>
        </item>
        <item>
            <title><![CDATA[MoQ: Refactoring the Internet's real-time media stack]]></title>
            <link>https://blog.cloudflare.com/moq/</link>
            <pubDate>Fri, 22 Aug 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ Media over QUIC (MoQ) is a new IETF standard that resolves this conflict, creating a single foundation for sub-second, interactive streaming at a global scale.
 ]]></description>
            <content:encoded><![CDATA[ <p>For over two decades, we've built real-time communication on the Internet using a patchwork of specialized tools. RTMP gave us ingest. <a href="https://www.cloudflare.com/learning/video/what-is-http-live-streaming/"><u>HLS</u></a> and <a href="https://www.mpeg.org/standards/MPEG-DASH/"><u>DASH</u></a> gave us scale. WebRTC gave us interactivity. Each solved a specific problem for its time, and together they power the global streaming ecosystem we rely on today.</p><p>But using them together in 2025 feels like building a modern application with tools from different eras. The seams are starting to show—in complexity, in latency, and in the flexibility needed for the next generation of applications, from sub-second live auctions to massive interactive events. We're often forced to make painful trade-offs between latency, scale, and operational complexity.</p><p>Today Cloudflare is launching the first Media over QUIC (MoQ) relay network, running on every Cloudflare server in datacenters in 330+ cities. MoQ is an open protocol being developed at the <a href="https://www.ietf.org/"><u>IETF</u></a> by engineers from across the industry—not a proprietary Cloudflare technology. MoQ combines the low-latency interactivity of WebRTC, the scalability of HLS/DASH, and the simplicity of a single architecture, all built on a modern transport layer. We're joining Meta, Google, Cisco, and others in building implementations that work seamlessly together, creating a shared foundation for the next generation of real-time applications on the Internet.</p>
    <div>
      <h3><b>An evolutionary ladder of compromise</b></h3>
      <a href="#an-evolutionary-ladder-of-compromise">
        
      </a>
    </div>
    <p>To understand the promise of MoQ, we first have to appreciate the history that led us here—a journey defined by a series of architectural compromises where solving one problem inevitably created another.</p><p><b>The RTMP era: Conquering latency, compromising on scale</b></p><p>In the early 2000s, <b>RTMP (Real-Time Messaging Protocol)</b> was a breakthrough. It solved the frustrating "download and wait" experience of early video playback on the web by creating a persistent, stateful TCP connection between a <a href="https://en.wikipedia.org/wiki/Adobe_Flash"><u>Flash</u></a> client and a server. This enabled low-latency streaming (2-5 seconds), powering the first wave of live platforms like <a href="http://justin.tv"><u>Justin.tv</u></a> (which later became Twitch).</p><p>But its strength was its weakness. That stateful connection, which had to be maintained for every viewer, was architecturally hostile to scale. It required expensive, specialized media servers and couldn't use the commodity HTTP-based <a href="https://www.cloudflare.com/learning/cdn/what-is-a-cdn/"><u>Content Delivery Networks (CDNs)</u></a> that were beginning to power the rest of the web. Its reliance on TCP also meant that a single lost packet could freeze the entire stream—a phenomenon known as <a href="https://blog.cloudflare.com/the-road-to-quic/#head-of-line-blocking"><u>head-of-line blocking</u></a>—creating jarring latency spikes. The industry retained RTMP for the "first mile" from the camera to servers (ingest), but a new solution was needed for the "last mile" from servers to your screen (delivery).</p><p><b>The HLS &amp; DASH era: Solving for scale, compromising on latency</b></p><p>The catalyst for the next era was the iPhone's rejection of Flash. In response, Apple created <a href="https://www.cloudflare.com/learning/video/what-is-http-live-streaming/"><b><u>HLS (HTTP Live Streaming)</u></b></a>. HLS, and its open-standard counterpart <b>MPEG-DASH</b> abandoned stateful connections and treated video as a sequence of small, static files delivered over standard HTTP.</p><p>This enabled much greater scalability. By moving to the interoperable open standard of HTTP for the underlying transport, video could now be distributed by any web server and cached by global CDNs, allowing platforms to reach millions of viewers reliably and relatively inexpensively. The compromise? A <i>significant</i> trade-off in latency. To ensure smooth playback, players needed to buffer at least three video segments before starting. With segment durations of 6-10 seconds, this baked 15-30 seconds of latency directly into the architecture.</p><p>While extensions like <a href="https://developer.apple.com/documentation/http-live-streaming/enabling-low-latency-http-live-streaming-hls"><u>Low-Latency HLS (LL-HLS)</u></a> have more recently emerged to achieve latencies in the 3-second range, they remain complex patches<a href="https://blog.cloudflare.com/the-road-to-quic/#head-of-line-blocking"><u> fighting against the protocol's fundamental design</u></a>. These extensions introduce a layer of stateful, real-time communication—using clever workarounds like holding playlist requests open—that ultimately strain the stateless request-response model central to HTTP's scalability and composability.</p><p><b>The WebRTC Era: Conquering conversational latency, compromising on architecture</b></p><p>In parallel, <b>WebRTC (Web Real-Time Communication)</b> emerged to solve a different problem: plugin-free, two-way conversational video with sub-500ms latency within a browser. It worked by creating direct peer-to-peer (P2P) media paths, removing central servers from the equation.</p><p>But this P2P model is fundamentally at odds with broadcast scale. <a href="https://blog.cloudflare.com/cloudflare-calls-anycast-webrtc/#webrtc-growing-pains"><u>In a mesh network, the number of connections grows quadratically with each new participant</u></a> (the "N-squared problem"). For more than a handful of users, the model collapses under the weight of its own complexity. To work around this, the industry developed server-based topologies like the Selective Forwarding Unit (SFU) and Multipoint Control Unit (MCU). These are effective but require building what is essentially a <a href="https://blog.cloudflare.com/cloudflare-calls-anycast-webrtc/#is-cloudflare-calls-a-real-sfu"><u>private, stateful, real-time CDN</u></a>—a complex and expensive undertaking that is not standardized across infrastructure providers.</p><p>This journey has left us with a fragmented landscape of specialized, non-interoperable silos, forcing developers to stitch together multiple protocols and accept a painful three-way tension between <b>latency, scale, and complexity</b>.</p>
    <div>
      <h3><b>Introducing MoQ</b></h3>
      <a href="#introducing-moq">
        
      </a>
    </div>
    <p>This is the context into which Media over QUIC (MoQ) emerges. It's not just another protocol; it's a new design philosophy built from the ground up to resolve this historical trilemma. Born out of an open, community-driven effort at the IETF, <u>MoQ aims to be a foundational Internet technology, not a proprietary product</u>.</p><p>Its promise is to unify the disparate worlds of streaming by delivering:</p><ol><li><p><b>Sub-second latency at broadcast scale:</b> Combining the latency of WebRTC with the scale of HLS/DASH and the simplicity of RTMP.</p></li><li><p><b>Architectural simplicity:</b> Creating a single, flexible protocol for ingest, distribution, and interactive use cases, eliminating the need to transcode between different technologies.</p></li><li><p><b>Transport efficiency:</b> Building on <a href="https://blog.cloudflare.com/the-road-to-quic/"><u>QUIC</u></a>, a <a href="https://www.cloudflare.com/learning/ddos/glossary/user-datagram-protocol-udp/"><u>UDP</u></a> based protocol to eliminate bottlenecks like TCP<a href="https://blog.cloudflare.com/the-road-to-quic/#head-of-line-blocking"><u> head-of-line blocking</u></a>.</p></li></ol><p>The initial focus was "Media" over QUIC, but the core concepts—named tracks of timed, ordered, but independent data—are so flexible that the working group is now simply calling the protocol "MoQ." The name reflects the power of the abstraction: it's a generic transport for any real-time data that needs to be delivered efficiently and at scale.</p><p>MoQ is now generic enough that it’s a data fanout or pub/sub system, for everything from audio/video (high bandwidth data) to sports score updates (low bandwidth data).</p>
    <div>
      <h3><b>A deep dive into the MoQ protocol stack</b></h3>
      <a href="#a-deep-dive-into-the-moq-protocol-stack">
        
      </a>
    </div>
    <p>MoQ's elegance comes from solving the right problem at the right layer. Let's build up from the foundation to see how it achieves sub-second latency at scale.</p><p>The choice of QUIC as MoQ's foundation isn't arbitrary—it addresses issues that have plagued streaming protocols for decades.</p><p>By building on <b>QUIC</b> (the transport protocol that also powers <a href="https://www.cloudflare.com/learning/performance/what-is-http3/"><u>HTTP/3</u></a>), MoQ solves some key streaming problems:</p><ul><li><p><b>No head-of-line blocking:</b> Unlike TCP where one lost packet blocks everything behind it, QUIC streams are independent. A lost packet on one stream (e.g., an audio track) doesn't block another (e.g., the main video track). This alone eliminates the stuttering that plagued RTMP.</p></li><li><p><b>Connection migration:</b> When your device switches from Wi-Fi to cellular mid-stream, the connection seamlessly migrates without interruption—no rebuffering, no reconnection.</p></li><li><p><b>Fast connection establishment:</b> QUIC's <a href="https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/"><u>0-RTT resumption</u></a> means returning viewers can start playing instantly.</p></li><li><p><b>Baked-in, mandatory encryption:</b> All QUIC connections are encrypted by default with <a href="https://blog.cloudflare.com/rfc-8446-aka-tls-1-3/"><u>TLS 1.3</u></a>.</p></li></ul>
    <div>
      <h4>The core innovation: Publish/subscribe for media</h4>
      <a href="#the-core-innovation-publish-subscribe-for-media">
        
      </a>
    </div>
    <p>With QUIC solving transport issues, MoQ introduces its key innovation: treating media as subscribable tracks in a publish/subscribe system. But unlike traditional pub/sub, this is designed specifically for real-time media at CDN scale.</p><p>Instead of complex session management (WebRTC) or file-based chunking (HLS), <b>MoQ lets publishers announce named tracks of media that subscribers can request</b>. A relay network handles the distribution without needing to understand the media itself.</p>
    <div>
      <h4>How MoQ organizes media: The data model</h4>
      <a href="#how-moq-organizes-media-the-data-model">
        
      </a>
    </div>
    <p>Before we see how media flows through the network, let's understand how MoQ structures it. MoQ organizes data in a hierarchy:</p><ul><li><p><b>Tracks</b>: Named streams of media, like "video-1080p" or "audio-english". Subscribers request specific tracks by name.</p></li><li><p><b>Groups</b>: Independently decodable chunks of a track. For video, this typically means a GOP (Group of Pictures) starting with a keyframe. New subscribers can join at any Group boundary.</p></li><li><p><b>Objects</b>: The actual packets sent on the wire. Each Object belongs to a Track and has a position within a Group.</p></li></ul><p>This simple hierarchy enables two capabilities:</p><ol><li><p>Subscribers can start playback at <b>Group</b> boundaries without waiting for the next keyframe</p></li><li><p>Relays can forward <b>Objects</b> without parsing or understanding the media format</p></li></ol>
    <div>
      <h5>The network architecture: From publisher to subscriber</h5>
      <a href="#the-network-architecture-from-publisher-to-subscriber">
        
      </a>
    </div>
    <p>MoQ’s network components are also simple:</p><ul><li><p><b>Publishers</b>: Announce track namespaces and send Objects</p></li><li><p><b>Subscribers</b>: Request specific tracks by name</p></li><li><p><b>Relays</b>: Connect publishers to subscribers by forwarding immutable Objects without parsing or <a href="https://www.cloudflare.com/learning/video/video-encoding-formats/"><u>transcoding</u></a> the media</p></li></ul><p>A Relay acts as a subscriber to receive tracks from upstream (like the original publisher) and simultaneously acts as a publisher to forward those same tracks downstream. This model is the key to MoQ's scalability: one upstream subscription can fan out to serve thousands of downstream viewers.</p>
    <div>
      <h5>The MoQ Stack</h5>
      <a href="#the-moq-stack">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4g2MroH24otkzH3LQsFWZe/84ca43ad6c1c933ac395bf4ac767c584/image1.png" />
          </figure><p>MoQ's architecture can be understood as three distinct layers, each with a clear job:</p><ol><li><p><b>The Transport Foundation (QUIC or WebTransport):</b> This is the modern foundation upon which everything is built. MoQT can run directly over raw <b>QUIC</b>, which is ideal for native applications, or over <b>WebTransport</b>, which is required for use in a web browser. Crucially, the<a href="https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-02.html"> <u>WebTransport protocol</u></a> and its corresponding<a href="https://w3c.github.io/webtransport/"> <u>W3C browser API</u></a> make QUIC's multiplexed reliable streams and unreliable datagrams directly accessible to browser applications. This is a game-changer. Protocols like <a href="https://blog.cloudflare.com/stream-now-supports-srt-as-a-drop-in-replacement-for-rtmp/"><u>SRT</u></a> may be efficient, but their lack of native browser support relegates them to ingest-only roles. WebTransport gives MoQ first-class citizenship on the web, making it suitable for both ingest and massive-scale distribution directly to clients.</p></li><li><p><b>The MoQT Layer:</b> Sitting on top of QUIC (or WebTransport), the MoQT layer provides the signaling and structure for a publish-subscribe system. This is the primary focus of the IETF working group. It defines the core control messages—like ANNOUNCE, and SUBSCRIBE—and the basic data model we just covered. MoQT itself is intentionally spartan; it doesn't know or care if the data it's moving is <a href="https://www.cloudflare.com/learning/video/what-is-h264-avc/"><u>H.264</u></a> video, Opus audio, or game state updates.</p></li><li><p><b>The Streaming Format Layer:</b> This is where media-specific logic lives. A streaming format defines things like manifests, codec metadata, and packaging rules.
 <a href="https://datatracker.ietf.org/doc/draft-ietf-moq-warp/"><b><u>WARP</u></b></a> is one such format being developed alongside MoQT at the IETF, but it isn't the only one. Another standards body, like DASH-IF, could define a <a href="https://www.iso.org/standard/85623.html"><u>CMAF</u></a>-based streaming format over MoQT. A company that controls both original publisher and end subscriber can develop its own proprietary streaming format to experiment with new codecs or delivery mechanisms without being constrained by the transport protocol.</p></li></ol><p>This separation of layers is why different organizations can build interoperable implementations while still innovating at the streaming format layer.</p>
    <div>
      <h4>End-to-End Data Flow</h4>
      <a href="#end-to-end-data-flow">
        
      </a>
    </div>
    <p>Now that we understand the architecture and the data model, let's walk through how these pieces come together to deliver a stream. The protocol is flexible, but a typical broadcast flow relies on the <code>ANNOUNCE</code> and <code>SUBSCRIBE </code>messages to establish a data path from a publisher to a subscriber through the relay network.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2iRTJFdtCjIOcyg7ezoYgJ/e303ea8d1eb438328b60fdb28be47e84/image2.png" />
          </figure><p>Here is a step-by-step breakdown of what happens in this flow:</p><ol><li><p><b>Initiating Connections:</b> The process begins when the endpoints, acting as clients, connect to the relay network. The Original Publisher initiates a connection with its nearest relay (we'll call it Relay A). Separately, an End Subscriber initiates a connection with its own local relay (Relay B). These endpoints perform a <code>SETUP</code> handshake with their respective relays to establish a MoQ session and declare supported parameters.</p></li><li><p><b>Announcing a Namespace:</b> To make its content discoverable, the Publisher sends an <code>ANNOUNCE</code> message to Relay A. This message declares that the publisher is the authoritative source for a given <b>track namespace</b>. Relay A receives this and registers in a shared control plane (a conceptual database) that it is now a source for this namespace within the network.</p></li><li><p><b>Subscribing to a Track:</b> When the End Subscriber wants to receive media, it sends a <code>SUBSCRIBE</code> message to its relay, Relay B. This message is a request for a specific <b>track name</b> within a specific <b>track namespace</b>.</p></li><li><p><b>Connecting the Relays:</b> Relay B receives the <code>SUBSCRIBE</code> request and queries the control plane. It looks up the requested namespace and discovers that Relay A is the source. Relay B then initiates a session with Relay A (if it doesn't already have one) and forwards the <code>SUBSCRIBE</code> request upstream.</p></li><li><p><b>Completing the Path and Forwarding Objects:</b> Relay A, having received the subscription request from Relay B, forwards it to the Original Publisher. With the full path now established, the Publisher begins sending the <code>Objects</code> for the requested track. The Objects flow from the Publisher to Relay A, which forwards them to Relay B, which in turn forwards them to the End Subscriber. If another subscriber connects to Relay B and requests the same track, Relay B can immediately start sending them the Objects without needing to create a new upstream subscription.</p></li></ol>
    <div>
      <h5>An Alternative Flow: The <code>PUBLISH</code> Model</h5>
      <a href="#an-alternative-flow-the-publish-model">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6KJYU1eWNyuSZEHNYonDDn/3898003d5a7f5904787c7ef009b22fe0/image3.png" />
          </figure><p>More recent drafts of the MoQ specification have introduced an alternative, push-based model using a <code>PUBLISH</code> message. In this flow, a publisher can effectively ask for permission to send a track's objects to a relay <i>without</i> waiting for a <code>SUBSCRIBE </code>request. The publisher sends a <code>PUBLISH</code> message, and the relay's <code>PUBLISH_OK</code> response indicates whether it will accept the objects. This is particularly useful for ingest scenarios, where a publisher wants to send its stream to an entry point in the network immediately, ensuring the media is available the instant the first subscriber connects.</p>
    <div>
      <h4>Advanced capabilities: Prioritization and congestion control</h4>
      <a href="#advanced-capabilities-prioritization-and-congestion-control">
        
      </a>
    </div>
    <p>MoQ’s benefits really shine when networks get congested. MoQ includes mechanisms for handling the reality of network traffic. One such mechanism is Subgroups.</p><p><b>Subgroups</b> are subdivisions within a Group that effectively map directly to the underlying QUIC streams. All Objects within the same Subgroup are generally sent on the same QUIC stream, guaranteeing their delivery order. Subgroup numbering also presents an opportunity to encode prioritization: within a Group, lower-numbered Subgroups are considered higher priority. </p><p>This enables intelligent quality degradation, especially with layered codecs (e.g. SVC):</p><ul><li><p><b>Subgroup 0</b>: Base video layer (360p) - must deliver</p></li><li><p><b>Subgroup 1</b>: Enhancement to 720p - deliver if bandwidth allows</p></li><li><p><b>Subgroup 2</b>: Enhancement to 1080p - first to drop under congestion</p></li></ul><p>When a relay detects congestion, it can drop Objects from higher-numbered Subgroups, preserving the base layer. Viewers see reduced quality instead of buffering.</p><p>The MoQ specification defines a scheduling algorithm that determines the order for all objects that are "ready to send." When a relay has multiple objects ready, it prioritizes them first by <b>group order</b> (ascending or descending) and then, within a group, by <b>subgroup id</b>. Our implementation supports the <b>group order</b> preference, which can be useful for low-latency broadcasts. If a viewer falls behind and its subscription uses descending group order, the relay prioritizes sending Objects from the newest "live" Group, potentially canceling unsent Objects from older Groups. This can help viewers catch up to the live edge quickly, a highly desirable feature for many interactive streaming use cases. The optimal strategies for using these features to improve QoE for specific use cases are still an open research question. We invite developers and researchers to use our network to experiment and help find the answers.</p>
    <div>
      <h3><b>Implementation: building the Cloudflare MoQ relay</b></h3>
      <a href="#implementation-building-the-cloudflare-moq-relay">
        
      </a>
    </div>
    <p>Theory is one thing; implementation is another. To validate the protocol and understand its real-world challenges, we've been building one of the first global MoQ relay networks. Cloudflare's network, which places compute and logic at the edge, is very well suited for this.</p><p>Our architecture connects the abstract concepts of MoQ to the Cloudflare stack. In our deep dive, we mentioned that when a publisher <code>ANNOUNCE</code>s a namespace, relays need to register this availability in a "shared control plane" so that <code>SUBSCRIBE</code> requests can be routed correctly. For this critical piece of state management, we use <a href="https://developers.cloudflare.com/durable-objects/"><u>Durable Objects</u></a>.</p><p>When a publisher announces a new namespace to a relay in, say, London, that relay uses a Durable Object—our strongly consistent, single-threaded storage solution—to record that this namespace is now available at that specific location. When a subscriber in Paris wants a track from that namespace, the network can query this distributed state to find the nearest source and route the <code>SUBSCRIBE</code> request accordingly. This architecture builds upon the technology we developed for Cloudflare's real-time services and provides a solution to the challenge of state management at a global scale.</p>
    <div>
      <h4>An Evolving Specification</h4>
      <a href="#an-evolving-specification">
        
      </a>
    </div>
    <p>Building on a new protocol in the open means implementing against a moving target. To get MoQ into the hands of the community, we made a deliberate trade-off: our current relay implementation is based on a <b>subset of the features defined in </b><a href="https://www.ietf.org/archive/id/draft-ietf-moq-transport-07.html"><b><u>draft-ietf-moq-transport-07</u></b></a>. This version became a de facto target for interoperability among several open-source projects and pausing there allowed us to put effort towards other aspects of deploying our relay network<b>.</b></p><p>This draft of the protocol makes a distinction between accessing "past" and "future" content. <code><b>SUBSCRIBE</b></code> is used to receive <b>future</b> objects for a track as they arrive—like tuning into a live broadcast to get everything from that moment forward. In contrast, <code><b>FETCH</b></code> provides a mechanism for accessing <b>past</b> content that a relay may already have in its cache—like asking for a recording of a song that just played.</p><p>Both are part of the same specification, but for the most pressing low-latency use cases, a performant implementation of <code>SUBSCRIBE</code> is what matters most. For that reason, we have focused our initial efforts there and have not yet implemented <code>FETCH</code>.</p><p>This is where our roadmap is flexible and where the community can have a direct impact. Do you need <code>FETCH</code> to build on-demand or catch-up functionality? Or is more complete support for the prioritization features within <code>SUBSCRIBE</code> more critical for your use case? The feedback we receive from early developers will help us decide what to build next.</p><p>As always, we will announce our updates and changes to our implementation as we continue with development on our <a href="https://developers.cloudflare.com/moq"><u>developer docs pages</u></a>.</p>
    <div>
      <h3>Kick the tires on the future</h3>
      <a href="#kick-the-tires-on-the-future">
        
      </a>
    </div>
    <p>We believe in building in the open and interoperability in the community. MoQ is not a Cloudflare technology but a foundational Internet technology. To that end, the first demo client we’re presenting is an open source, community example.</p><p><b>You can access the demo here: </b><a href="https://moq.dev/publish/"><b><u>https://moq.dev/publish/</u></b></a></p><p>Even though this is a preview release, we are running MoQ relays at Cloudflare’s full scale, like we do every production service. This means every server that is part of the Cloudflare network in more than 330 cities is now a MoQ relay.</p><p>We invite you to experience the "wow" moment of near-instant, sub-second streaming latency that MoQ enables. How would you use a protocol that offers the speed of a video call with the scale of a global broadcast?</p>
    <div>
      <h3><b>Interoperability</b></h3>
      <a href="#interoperability">
        
      </a>
    </div>
    <p>We’ve been working with others in the IETF WG community and beyond on interoperability of publishers, players and other parts of the MoQ ecosystem. So far, we’ve tested with:</p><ul><li><p>Luke Curley’s <a href="https://moq.dev"><u>moq.dev</u></a></p></li><li><p>Lorenzo Miniero’s <a href="https://github.com/meetecho/imquic"><u>imquic</u></a></p></li><li><p>Meta’s <a href="https://github.com/facebookexperimental/moxygen"><u>Moxygen</u></a> </p></li><li><p><a href="https://github.com/englishm/moq-rs"><u>moq-rs</u></a></p></li><li><p><a href="https://github.com/englishm/moq-js"><u>moq-js</u></a></p></li><li><p><a href="https://norsk.video/"><u>Norsk</u></a></p></li><li><p><a href="https://vindral.com/"><u>Vindral</u></a></p></li></ul>
    <div>
      <h3>The Road Ahead</h3>
      <a href="#the-road-ahead">
        
      </a>
    </div>
    <p>The Internet's media stack is being refactored. For two decades, we've been forced to choose between latency, scale, and complexity. The compromises we made solved some problems, but also led to a fragmented ecosystem.</p><p>MoQ represents a promising new foundation—a chance to unify the silos and build the next generation of real-time applications on a scalable protocol. We're committed to helping build this foundation in the open, and we're just getting started.</p><p>MoQ is a realistic way forward, built on QUIC for future proofing, easier to understand than WebRTC, compatible with browsers unlike RTMP.</p><p>The protocol is evolving, the implementations are maturing, and the community is growing. Whether you're building the next generation of live streaming, exploring real-time collaboration, or pushing the boundaries of interactive media, consider whether MoQ may provide the foundation you need.</p>
    <div>
      <h3>Availability and pricing</h3>
      <a href="#availability-and-pricing">
        
      </a>
    </div>
    <p>We want developers to start building with MoQ today. To make that possible MoQ at Cloudflare is in tech preview - this means it's available free of charge for testing (at any scale). Visit our <a href="https://developers.cloudflare.com/moq/"><u>developer homepage </u></a>for updates and potential breaking changes.</p><p>Indie developers and large enterprises alike ask about pricing early in their adoption of new technologies. We will be transparent and clear about MoQ pricing. In general availability, self-serve customers should expect to pay 5 cents/GB outbound with no cost for traffic sent towards Cloudflare. </p><p>Enterprise customers can expect usual pricing in line with regular media delivery pricing, competitive with incumbent protocols. This means if you’re already using Cloudflare for media delivery, you should not be wary of adopting new technologies because of cost. We will support you.</p><p>If you’re interested in partnering with Cloudflare in adopting the protocol early or contributing to its development, please reach out to us at <a href="#"><u>moq@cloudflare.com</u></a>! Engineers excited about the future of the Internet are standing by.</p>
    <div>
      <h3>Get involved:</h3>
      <a href="#get-involved">
        
      </a>
    </div>
    <ul><li><p><b>Try the demo:</b> <a href="https://moq.dev/publish/"><u>https://moq.dev/publish/</u></a></p></li><li><p><b>Read the Internet draft:</b> <a href="https://datatracker.ietf.org/doc/draft-ietf-moq-transport/"><u>https://datatracker.ietf.org/doc/draft-ietf-moq-transport/</u></a></p></li><li><p><b>Contribute</b> to the protocol’s development: <a href="https://datatracker.ietf.org/group/moq/documents/"><u>https://datatracker.ietf.org/group/moq/documents/</u></a></p></li><li><p><b>Visit </b>our developer homepage: <a href="https://developers.cloudflare.com/moq/"><u>https://developers.cloudflare.com/moq/</u></a></p></li></ul><p></p> ]]></content:encoded>
            <category><![CDATA[Video]]></category>
            <category><![CDATA[QUIC]]></category>
            <category><![CDATA[Live Streaming]]></category>
            <category><![CDATA[WebRTC]]></category>
            <category><![CDATA[IETF]]></category>
            <category><![CDATA[Standards]]></category>
            <guid isPermaLink="false">2XgF5NjmAy3cqybLPkpMFu</guid>
            <dc:creator>Mike English</dc:creator>
            <dc:creator>Renan Dincer</dc:creator>
        </item>
        <item>
            <title><![CDATA[New URLPattern API brings improved pattern matching to Node.js and Cloudflare Workers]]></title>
            <link>https://blog.cloudflare.com/improving-web-standards-urlpattern/</link>
            <pubDate>Mon, 24 Mar 2025 13:00:00 GMT</pubDate>
            <description><![CDATA[ Today we're announcing our latest contribution to Node.js, now available in v23.8.0: URLPattern.  ]]></description>
            <content:encoded><![CDATA[ <p>Today, we are excited to announce that we have contributed an implementation of the <a href="https://urlpattern.spec.whatwg.org"><u>URLPattern</u></a> API to Node.js, and it is available starting with <a href="https://nodejs.org/en/blog/release/v23.8.0"><u>the v23.8.0 update</u></a>. We've done this by adding our URLPattern implementation to <a href="https://github.com/ada-url/ada"><u>Ada URL</u></a>, the high-performance URL parser that now powers URL handling in both Node.js and Cloudflare Workers. This marks an important step toward bringing this API to the broader JavaScript ecosystem.</p><p>Cloudflare Workers has, from the beginning, embraced a standards-based JavaScript programming model, and Cloudflare was one of the founding companies for what has evolved into <a href="https://ecma-international.org/technical-committees/tc55/"><u>ECMA's 55th Technical Committee</u></a>, focusing on interoperability between Web-interoperable runtimes like Workers, Node.js, Deno, and others. This contribution highlights and marks our commitment to this ongoing philosophy. Ensuring that all the JavaScript runtimes work consistently and offer at least a minimally consistent set of features is critical to ensuring the ongoing health of the ecosystem as a whole.</p><p>URLPattern API contribution is just one example of Cloudflare’s ongoing commitment to the open-source ecosystem. We actively contribute to numerous open-source projects including Node.js, V8, and Ada URL, while also maintaining our own open-source initiatives like <a href="https://github.com/cloudflare/workerd"><u>workerd</u></a> and <a href="https://github.com/cloudflare/workers-sdk"><u>wrangler</u></a>. By upstreaming improvements to foundational technologies that power the web, we strengthen the entire developer ecosystem while ensuring consistent features across JavaScript runtimes. This collaborative approach reflects our belief that open standards and shared implementations benefit everyone - reducing fragmentation, improving developer experience and creating a better Internet. </p>
    <div>
      <h2>What is URLPattern?</h2>
      <a href="#what-is-urlpattern">
        
      </a>
    </div>
    <p>URLPattern is a standard published by the <a href="https://whatwg.org/"><u>WHATWG (Web Hypertext Application Technology Working Group)</u></a> which provides a pattern-matching system for URLs. This specification is available at <a href="http://urlpattern.spec.whatwg.org"><u>urlpattern.spec.whatwg.org</u></a>. The API provides developers with an easy-to-use, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"><u>regular expression (regex)</u></a>-based approach to handling route matching, with built-in support for named parameters, wildcards, and more complex pattern matching that works uniformly across all URL components.</p><p>URLPattern is part of the <a href="https://min-common-api.proposal.wintertc.org/"><u>WinterTC Minimum Common API</u></a>, a soon-to-be standardized subset of web platform APIs designed to ensure interoperability across JavaScript runtimes, particularly for server-side and non-browser environments, and includes other APIs such as <a href="https://url.spec.whatwg.org/#url"><u>URL</u></a> and <a href="https://url.spec.whatwg.org/#urlsearchparams"><u>URLSearchParams</u></a>.</p><p>Cloudflare Workers has supported URLPattern for a number of years now, reflecting our commitment to enabling developers to use standard APIs across both browsers and server-side JavaScript runtimes. Contributing to Node.js and unifying the URLPattern implementation simplifies the ecosystem by reducing fragmentation, while at the same time improving our own implementation in Cloudflare Workers by making it faster and more specification compliant.</p><p>The following example demonstrates how URLPattern is used by creating a pattern that matches URLs with a “/blog/:year/:month/:slug” path structure, then tests if one specific URL string matches this pattern, and extracts the named parameters from a second URL using the exec method.</p>
            <pre><code>const pattern = new URLPattern({
  pathname: '/blog/:year/:month/:slug'
});

if (pattern.test('https://example.com/blog/2025/03/urlpattern-launch')) {
  console.log('Match found!');
}

const result = pattern.exec('https://example.com/blog/2025/03/urlpattern-launch');
console.log(result.pathname.groups.year); // "2025"
console.log(result.pathname.groups.month); // "03"
console.log(result.pathname.groups.slug); // "urlpattern-launch"</code></pre>
            <p>The URLPattern constructor accepts pattern strings or objects defining patterns for individual URL components. The <code>test()</code> method returns a boolean indicating if a URL simply matches the pattern. The <code>exec()</code> method provides detailed match results including captured groups. Behind this simple API, there’s sophisticated machinery working behind the scenes:</p><ol><li><p>When a URLPattern is used, it internally breaks down a URL, matching it against eight distinct components: protocol, username, password, hostname, port, pathname, search, and hash. This component-based approach gives the developer control over which parts of a URL to match.</p></li><li><p>Upon creation of the instance, URLPattern parses your input patterns for each component and compiles them internally into eight specialized regular expressions (one for each component type). This compilation step happens just once when you create an URLPattern object, optimizing subsequent matching operations.</p></li><li><p>During a match operation (whether using <code>test()</code> or <code>exec()</code>), these regular expressions are used to determine if the input matches the given properties. The <code>test()</code> method tells you if there’s a match, while <code>exec()</code> provides detailed information about what was matched, including any named capture groups from your pattern.</p></li></ol>
    <div>
      <h2>Fixing things along the way</h2>
      <a href="#fixing-things-along-the-way">
        
      </a>
    </div>
    <p>While implementing URLPattern, we discovered some inconsistencies between the specification and the <a href="https://github.com/web-platform-tests/wpt/pull/49782"><u>web-platform tests</u></a>, a cross-browser test suite maintained by all major browsers to test conformance to web standard specifications. For instance, we found that <a href="https://github.com/whatwg/urlpattern/issues/240"><u>URLs with non-special protocols (opaque-paths)</u></a> and URLs with invalid characters in hostnames were not correctly defined and processed within the URLPattern specification. We worked actively with the Chromium and the Safari teams to address these issues.</p><p>URLPatterns constructed from hostname components that contain newline or tab characters were expected to fail in the corresponding web-platform tests. This was due to an inconsistency with the original URLPattern implementation and the URLPattern specification.</p>
            <pre><code>const pattern = new URL({ "hostname": "bad\nhostname" });
const matched = pattern.test({ "hostname": "badhostname" });
// This now returns true.</code></pre>
            <p>We opened <a href="https://github.com/whatwg/urlpattern/issues/239"><u>several issues</u></a> to document these inconsistencies and followed up with <a href="https://github.com/whatwg/urlpattern/pull/243"><u>a pull-request to fix the specification</u></a>, ensuring that all implementations will eventually converge on the same corrected behavior. This also resulted in fixing several inconsistencies in web-platform tests, particularly around handling certain types of white space (such as newline or tab characters) in hostnames. </p>
    <div>
      <h2>Getting started with URLPattern</h2>
      <a href="#getting-started-with-urlpattern">
        
      </a>
    </div>
    <p>If you’re interested in using URLPattern today, you can:</p><ul><li><p>Use it natively in modern browsers by accessing the global URLPattern class</p></li><li><p>Try it in Cloudflare Workers (which has had URLPattern support for some time, now with improved spec compliance and performance)</p></li><li><p>Try it in Node.js, <a href="https://nodejs.org/en/blog/release/v23.8.0"><u>starting from v23.8.0</u></a></p></li><li><p>Try it in NativeScript on iOS and Android, <a href="https://blog.nativescript.org/nativescript-8-9-announcement/"><u>starting from v8.9.0</u></a></p></li><li><p>Try it in <a href="https://docs.deno.com/api/web/~/URLPattern"><u>Deno</u></a></p></li></ul><p>Here is a more complex example showing how URLPattern can be used for routing in a Cloudflare Worker — a common use case when building API endpoints or web applications that need to handle different URL paths efficiently and differently. The following example shows a pattern for <a href="https://en.wikipedia.org/wiki/REST"><u>REST APIs</u></a> that matches both “/users” and “/users/:userId”</p>
            <pre><code>const routes = [
  new URLPattern({ pathname: '/users{/:userId}?' }),
];

export default {
  async fetch(request, env, ctx): Promise&lt;Response&gt; {
    const url = new URL(request.url);
    for (const route of routes) {
      const match = route.exec(url);
      if (match) {
        const { userId } = match.pathname.groups;
        if (userId) {
          return new Response(`User ID: ${userId}`);
        }
        return new Response('List of users');
      }
    }
    // No matching route found
    return new Response('Not Found', { status: 404 });
  },
} satisfies ExportedHandler&lt;Env&gt;;</code></pre>
            
    <div>
      <h2>What does the future hold?</h2>
      <a href="#what-does-the-future-hold">
        
      </a>
    </div>
    <p>The contribution of URLPattern to Ada URL and Node.js is just the beginning. We’re excited about the possibilities this opens up for developers across different JavaScript environments.</p><p>In the future, we expect to contribute additional improvements to URLPattern’s performance, enabling more use cases for web application routing. Additionally, efforts to standardize the <a href="https://github.com/whatwg/urlpattern/pull/166"><u>URLPatternList proposal</u></a> will help deliver faster matching capabilities for server-side runtimes. We’re excited about these developments and encourage you to try URLPattern in your projects today.	</p><p>Try it and let us know what you think by creating an issue on the <a href="https://github.com/cloudflare/workerd"><u>workerd repository</u></a>. Your feedback is invaluable as we work to further enhance URLPattern.</p><p>We hope to do our part to build a unified Javascript ecosystem, and encourage others to do the same. This may mean looking for opportunities, such as we have with URLPattern, to share API implementations across backend runtimes. It could mean using or contributing to <a href="https://web-platform-tests.org/"><u>web-platform-tests</u></a> if you are working on a server-side runtime or web-standard APIs, or it might mean joining <a href="https://wintertc.org/faq"><u>WinterTC</u></a> to help define web-interoperable standards for server-side JavaScript.</p> ]]></content:encoded>
            <category><![CDATA[Node.js]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Standards]]></category>
            <guid isPermaLink="false">55t98SAXi3erhs7Wn5dgno</guid>
            <dc:creator>Yagiz Nizipli</dc:creator>
            <dc:creator>James M Snell</dc:creator>
            <dc:creator>Daniel Lemire (Guest author)</dc:creator>
        </item>
        <item>
            <title><![CDATA[Enhance your website's security with Cloudflare’s free security.txt generator]]></title>
            <link>https://blog.cloudflare.com/security-txt/</link>
            <pubDate>Sun, 06 Oct 2024 23:00:00 GMT</pubDate>
            <description><![CDATA[ Cloudflare’s free security.txt generator lets users create and manage security.txt files. Enhance vulnerability disclosure, align with industry standards, and integrate into the dashboard. ]]></description>
            <content:encoded><![CDATA[ 
    <div>
      <h2>A story of security and simplicity</h2>
      <a href="#a-story-of-security-and-simplicity">
        
      </a>
    </div>
    <p>Meet Georgia, a diligent website administrator at a growing e-commerce company. Every day, Georgia juggles multiple tasks, from managing server uptime to ensuring customer data security. One morning, Georgia receives an email from a security researcher who discovered a potential vulnerability on the website. The researcher struggled to find the right contact information, leading to delays in reporting the issue. Georgia realizes the need for a standardized way to communicate with security researchers, ensuring that vulnerabilities are reported swiftly and efficiently. This is where security.txt comes in.</p>
    <div>
      <h2>Why security.txt matters</h2>
      <a href="#why-security-txt-matters">
        
      </a>
    </div>
    <p><a href="https://securitytxt.org/"><u>Security.txt</u></a> is becoming a widely adopted standard among security-conscious organizations. By providing a common location and format for vulnerability disclosure information, it helps bridge the gap between security researchers and organizations. This initiative is supported by major companies and aligns with global security best practices. By offering an automated security.txt generator for free, we aim to empower all of our users to enhance their security measures without additional costs.</p><p>In 2020, Cloudflare published the Cloudflare Worker for the security.txt generator as an <a href="https://github.com/cloudflare/securitytxt-worker?cf_history_state=%7B%22guid%22%3A%22C255D9FF78CD46CDA4F76812EA68C350%22%2C%22historyId%22%3A8%2C%22targetId%22%3A%22532D731DBD87B52B996FF5AD5ADDA824%22%7D"><u>open-source project on GitHub</u></a>, demonstrating our commitment to enhancing web security. This tool is actively used by Cloudflare to streamline vulnerability disclosure processes. However, over the past few years, we've observed a growing demand from our customers for an easier way to implement this standard. In response to this demand and to further support the adoption of security.txt across the Internet, we integrated it directly into our dashboard, making it simple for all our users to enhance their security practices. You can learn more about the initial release and its impact in our previous blog post <a href="https://blog.cloudflare.com/security-dot-txt/"><u>here</u></a>. </p>
    <div>
      <h3>Who can use the free Cloudflare security.txt generator</h3>
      <a href="#who-can-use-the-free-cloudflare-security-txt-generator">
        
      </a>
    </div>
    <p>This feature is designed for any Cloudflare user who manages a website, from <a href="https://www.cloudflare.com/small-business/">small business owners</a> to large enterprises, from developers to security professionals. Whether you're a seasoned security expert or new to website management, this tool provides an easy way to create and manage your security.txt file in your Cloudflare account, ensuring that you're prepared to handle vulnerability reports effectively.</p>
    <div>
      <h3>Technical insights: leveraging Cloudflare’s tools</h3>
      <a href="#technical-insights-leveraging-cloudflares-tools">
        
      </a>
    </div>
    <p>Our security.txt generator is seamlessly integrated into our dashboard. Here's how it works:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2z7tEph5hu4T7LCkZU5KFQ/8bc9c8efe332cda618c5dd8bb51e38da/image1.png" />
          </figure><p>When the user enters their data in the Cloudflare Dashboard, the information is immediately stored in a highly available and geo-redundant <a href="https://blog.cloudflare.com/performance-isolation-in-a-multi-tenant-database-environment/"><u>PostgreSQL database</u></a>. This ensures that all user data is securely kept and can be accessed quickly from any location within our global network.</p><p>Instead of creating a static file at the point of data entry, we use a dynamic approach. When a request for the security.txt file is made via the standard .well-known path specified by <a href="https://www.rfc-editor.org/rfc/rfc9116"><u>RFC 9116</u></a>, our system dynamically constructs the file using the latest data from our database. This method ensures that any updates made by users are reflected in real-time without requiring manual intervention or file regeneration. The data entered by users is synchronized across Cloudflare’s global network using our <a href="https://blog.cloudflare.com/introducing-quicksilver-configuration-distribution-at-internet-scale/"><u>Quicksilver</u></a> technology. This allows for rapid propagation of changes, ensuring that any updates to the security.txt file are available almost instantaneously across all servers.</p><p>Each security.txt file includes an expiration timestamp, which is set during the initial configuration. This timestamp helps alert users when their information may be outdated, encouraging them to review and update their details regularly. For example, if a user sets an expiration date 365 days into the future, they will receive notifications as this date approaches, prompting them to refresh their information.</p><p>To ensure compliance with best practices, we also support optional fields such as encryption keys and signatures within the security.txt file. Users can link to their PGP keys for secure communications or include signatures to verify authenticity, enhancing trust with security researchers.</p><p>Users who prefer automation can manage their security.txt files through our <a href="https://developers.cloudflare.com/api/operations/update-security-txt"><u>API</u></a>, allowing seamless integration with existing workflows and tools. This feature enables developers to programmatically update their security.txt configurations without manual dashboard interactions.</p><p>Users can also find a view of any missing security.txt files via <a href="https://developers.cloudflare.com/security-center/security-insights/"><u>Security Insights</u></a> under Security Center.</p>
    <div>
      <h3>Available now, and free for all Cloudflare users</h3>
      <a href="#available-now-and-free-for-all-cloudflare-users">
        
      </a>
    </div>
    <p>By making this feature available to all our users at no cost, we aim to support the security efforts of our entire community, helping you protect your digital assets and foster trust with your audience.</p><p>With the introduction of our free security.txt generator, we're taking a significant step towards simplifying security management for everyone. Whether you're a small business owner or a large enterprise, this tool empowers you to adopt industry best practices and ensure that you're ready to handle vulnerability reports effectively. <a href="https://developers.cloudflare.com/security-center/infrastructure/security-file/"><u>Set up security.txt</u></a> on your websites today!</p> ]]></content:encoded>
            <category><![CDATA[Better Internet]]></category>
            <category><![CDATA[Security Posture]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Standards]]></category>
            <category><![CDATA[security.txt]]></category>
            <guid isPermaLink="false">1uvkAn3IB6vSEO91XsPyAO</guid>
            <dc:creator>Alexandra Moraru</dc:creator>
            <dc:creator>Sam Khawasé</dc:creator>
        </item>
        <item>
            <title><![CDATA[You can now use WebGPU in Cloudflare Workers]]></title>
            <link>https://blog.cloudflare.com/webgpu-in-workers/</link>
            <pubDate>Wed, 27 Sep 2023 13:00:56 GMT</pubDate>
            <description><![CDATA[ Today, we are introducing WebGPU support to Cloudflare Workers. This blog will explain why it's important, why we did it, how you can use it, and what comes next ]]></description>
            <content:encoded><![CDATA[ <p></p><p>The browser as an app platform is real and stronger every day; long gone are the Browser Wars. Vendors and standard bodies have done amazingly well over the last years, working together and advancing web standards with new <a href="https://www.cloudflare.com/learning/security/api/what-is-an-api/">APIs</a> that allow developers to build fast and powerful applications, finally comparable to those we got used to seeing in the native OS' environment.</p><p>Today, browsers can render web pages and run code that interfaces with an <a href="https://developer.mozilla.org/en-US/docs/Web/API">extensive catalog of modern Web APIs</a>. Things like networking, rendering accelerated graphics, or even accessing low-level hardware features like USB devices are all now possible within the browser sandbox.</p><p>One of the most exciting new browser APIs that browser vendors have been rolling out over the last months is WebGPU, a modern, low-level GPU programming interface designed for high-performance 2D and 3D graphics and general purpose GPU compute.</p><p>Today, we are introducing <a href="https://developer.chrome.com/blog/webgpu-release/">WebGPU</a> support to Cloudflare Workers. This blog will explain why it's important, why we did it, how you can use it, and what comes next.</p>
    <div>
      <h3>The history of the GPU in the browser</h3>
      <a href="#the-history-of-the-gpu-in-the-browser">
        
      </a>
    </div>
    <p>To understand why WebGPU is a big deal, we must revisit history and see how browsers went from relying only on the CPU for everything in the early days to taking advantage of GPUs over the years.</p><p>In 2011, <a href="https://en.wikipedia.org/wiki/WebGL">WebGL 1</a>, a limited port of <a href="https://www.khronos.org/opengles/">OpenGL ES 2.0</a>, was introduced, providing an API for fast, accelerated 3D graphics in the browser for the first time. By then, this was somewhat of a revolution in enabling gaming and 3D visualizations in the browser. Some of the most popular 3D animation frameworks, like <a href="https://threejs.org/">Three.js</a>, launched in the same period. Who doesn't remember going to the (now defunct) <a href="https://en.wikipedia.org/wiki/Google_Chrome_Experiments">Google Chrome Experiments</a> page and spending hours in awe exploring the demos? Another option then was using the Flash Player, which was still dominant in the desktop environment, and their <a href="https://en.wikipedia.org/wiki/Stage3D">Stage 3D</a> API.</p><p>Later, in 2017, based on the learnings and shortcomings of its predecessor, WebGL 2 was a significant upgrade and brought more advanced GPU capabilities like shaders and more flexible textures and rendering.</p><p>WebGL, however, has proved to be a steep and complex learning curve for developers who want to take control of things, do low-level 3D graphics using the GPU, and not use 3rd party abstraction libraries.</p><p>Furthermore and more importantly, with the advent of <a href="https://www.cloudflare.com/learning/ai/what-is-machine-learning/">machine learning</a> and cryptography, we discovered that GPUs are great not only at drawing graphics but can be used for other applications that can take advantage of things like high-speed data or blazing-fast matrix multiplications, and one can use them to perform general computation. This became known as <a href="https://en.wikipedia.org/wiki/General-purpose_computing_on_graphics_processing_units">GPGPU</a>, short for general-purpose computing on graphics processing units.</p><p>With this in mind, in the native desktop and mobile operating system worlds, developers started using more advanced frameworks like <a href="https://en.wikipedia.org/wiki/CUDA">CUDA</a>, <a href="https://developer.apple.com/metal/">Metal</a>, <a href="https://en.wikipedia.org/wiki/DirectX#DirectX_12">DirectX 12</a>, or <a href="https://www.vulkan.org/learn#key-resources">Vulkan</a>. WebGL stayed behind. To fill this void and bring the browser up to date, in 2017, companies like Google, Apple, Intel, Microsoft, Kronos, and Mozilla created the <a href="https://www.w3.org/community/gpu/">GPU for Web Community Working Group</a> to collaboratively design the successor of WebGL and create the next modern 3D graphics and computation capabilities APIs for the Web.</p>
    <div>
      <h3>What is WebGPU</h3>
      <a href="#what-is-webgpu">
        
      </a>
    </div>
    <p>WebGPU was developed with the following advantages in mind:</p><ul><li><p><b>Lower Level Access</b> - WebGPU provides lower-level, direct access to the GPU vs. the high-level abstractions in WebGL. This enables more control over GPU resources.</p></li><li><p><b>Multi-Threading</b> - WebGPU can leverage multi-threaded rendering and compute, allowing improved CPU/GPU parallelism compared to WebGL, which relies on a single thread.</p></li><li><p><b>Compute Shaders</b> - First-class support for general-purpose compute shaders for GPGPU tasks, not just graphics. WebGL compute is limited.</p></li><li><p><b>Safety</b> - WebGPU ensures memory and GPU access safety, avoiding common WebGL pitfalls.</p></li><li><p><b>Portability</b> - WGSL shader language targets cross-API portability across GPU vendors vs. GLSL in WebGL.</p></li><li><p><b>Reduced Driver Overhead</b> - The lower level Vulkan/Metal/D3D12 basis improves overhead vs. OpenGL drivers in WebGL.</p></li><li><p><b>Pipeline State Objects</b> - Predefined pipeline configs avoid per-draw driver overhead in WebGL.</p></li><li><p><b>Memory Management</b> - Finer-grained buffer and resource management vs. WebGL.</p></li></ul><p>The “too long didn't read” version is that WebGPU provides lower-level control over the GPU hardware with reduced overhead. It's safer, has multi-threading, is focused on compute, not just graphics, and has portability advantages compared to WebGL.</p><p>If these aren't reasons enough to get excited, developers are also looking at WebGPU as an option for native platforms, not just the Web. For instance, you can use this <a href="https://github.com/webgpu-native/webgpu-headers/blob/main/webgpu.h">C API</a> that mimics the JavaScript specification. If you think about this and the power of WebAssembly, you can effectively have a truly platform-agnostic GPU hardware layer that you can use to <a href="https://developer.chrome.com/blog/webgpu-cross-platform/">develop</a> platforms for any operating system or browser.</p>
    <div>
      <h3>More than just graphics</h3>
      <a href="#more-than-just-graphics">
        
      </a>
    </div>
    <p>As explained above, besides being a graphics API, WebGPU makes it possible to perform tasks such as:</p><ul><li><p><b>Machine Learning</b> - Implement ML applications like <a href="https://www.cloudflare.com/learning/ai/what-is-neural-network/">neural networks</a> and computer vision algorithms using WebGPU compute shaders and matrices.</p></li><li><p><b>Scientific Computing</b> - Perform complex scientific computation like physics simulations and mathematical modeling using the GPU.</p></li><li><p><b>High Performance Computing</b> - Unlock breakthrough performance for parallel workloads by connecting WebGPU to languages like Rust, C/C++ via <a href="https://webassembly.org/">WebAssembly</a>.</p></li></ul><p><a href="https://gpuweb.github.io/gpuweb/wgsl/">WGSL</a>, the shader language for WebGPU, is what enables the general-purpose compute feature. Shaders, or more precisely, <a href="https://www.khronos.org/opengl/wiki/Compute_Shader">compute shaders</a>, have no user-defined inputs or outputs and are used for computing arbitrary information. Here are <a href="https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html">some examples</a> of simple WebGPU compute shaders if you want to learn more.</p>
    <div>
      <h3>WebGPU in Workers</h3>
      <a href="#webgpu-in-workers">
        
      </a>
    </div>
    <p>We've been watching WebGPU since the API was published. Its general-purpose compute features perfectly fit our Workers' ecosystem and capabilities and align well with our vision of providing our customers multiple compute and hardware options and bringing GPU workloads to our global network, close to clients.</p><p>Cloudflare also has a track record of pioneering support for emerging web standards on our network and services, accelerating their adoption for our customers. Examples of these are <a href="https://developers.cloudflare.com/workers/runtime-apis/web-crypto/">Web Crypto API</a>, <a href="/introducing-http2/">HTTP/2</a>, <a href="/http3-the-past-present-and-future/">HTTP/3</a>, <a href="/introducing-tls-1-3/">TLS 1.3</a>, or <a href="/early-hints/">Early hints</a>, but <a href="https://developers.cloudflare.com/workers/runtime-apis/">there are more</a>.</p><p>Bringing WebGPU to Workers was both natural and timely. Today, we are announcing that we have released a version of <a href="https://github.com/cloudflare/workerd">workerd</a>, the open-sourced JavaScript / Wasm runtime that powers Cloudflare Workers, with <a href="https://github.com/cloudflare/workerd/tree/main/src/workerd/api/gpu">WebGPU support</a>, that you can start playing and developing applications with, locally.</p><p>Starting today anyone can run this on their personal computer and experiment with WebGPU-enabled workers. Implementing local development first allows us to put this API in the hands of our customers and developers earlier and get feedback that will guide the development of this feature for production use.</p><p>But before we dig into code examples, let's explain how we built it.</p>
    <div>
      <h3>How we built WebGPU on top of Workers</h3>
      <a href="#how-we-built-webgpu-on-top-of-workers">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2rem1ZyAcVa3LO7ue6OTC7/debccf54201fe93a9221a6dd01bc5338/image2-22.png" />
            
            </figure><p>To implement the WebGPU API, we took advantage of <a href="https://dawn.googlesource.com/dawn/">Dawn</a>, an open-source library backed by Google, the same used in Chromium and Chrome, that provides applications with an implementation of the WebGPU standard. It also provides the <a href="https://github.com/webgpu-native/webgpu-headers/blob/main/webgpu.h">webgpu.h</a> headers file, the de facto reference for all the other implementations of the standard.</p><p>Dawn can interoperate with Linux, MacOS, and Windows GPUs by interfacing with each platform's native GPU frameworks. For example, when an application makes a WebGPU draw call, Dawn will convert that draw command into the equivalent Vulkan, Metal, or Direct3D 12 API call, depending on the platform.</p><p>From an application standpoint, Dawn handles the interactions with the underlying native graphics APIs that communicate directly with the GPU drivers. Dawn essentially acts as a middle layer that translates the WebGPU API calls into calls for the platform's native graphics API.</p><p>Cloudflare <a href="/workerd-open-source-workers-runtime/">workerd</a> is the underlying open-source runtime engine that executes Workers code. It shares most of its code with the same runtime that powers Cloudflare Workers' production environment but with some changes designed to make it more portable to other environments. We then have release cycles that aim to synchronize both codebases; more on that later. Workerd is also used with <a href="https://github.com/cloudflare/workers-sdk">wrangler</a>, our command-line tool for building and interacting with Cloudflare Workers, to support local development.</p><p>The WebGPU code that interfaces with the Dawn library can be found <a href="https://github.com/cloudflare/workerd/tree/main/src/workerd/api/gpu">here</a>, and can easily be enabled with a flag, checked <a href="https://github.com/cloudflare/workerd/blob/main/src/workerd/api/global-scope.c%2B%2B#L728">here</a>.</p>
            <pre><code>jsg::Ref&lt;api::gpu::GPU&gt; Navigator::getGPU(CompatibilityFlags::Reader flags) {
  // is this a durable object?
  KJ_IF_MAYBE (actor, IoContext::current().getActor()) {
    JSG_REQUIRE(actor-&gt;getPersistent() != nullptr, TypeError,
                "webgpu api is only available in Durable Objects (no storage)");
  } else {
    JSG_FAIL_REQUIRE(TypeError, "webgpu api is only available in Durable Objects");
  };

  JSG_REQUIRE(flags.getWebgpu(), TypeError, "webgpu needs the webgpu compatibility flag set");

  return jsg::alloc&lt;api::gpu::GPU&gt;();
}</code></pre>
            <p>The WebGPU API can only be accessed using <a href="https://developers.cloudflare.com/durable-objects/">Durable Objects</a>, which are essentially global singleton instances of Cloudflare Workers. There are two important reasons for this:</p><ul><li><p>WebGPU code typically wants to store the state between requests, for example, loading an <a href="https://www.cloudflare.com/learning/ai/what-is-artificial-intelligence/">AI model</a> into the GPU memory once and using it multiple times for inference.</p></li><li><p>Not all Cloudflare servers have GPUs yet, so although the worker that receives the request is typically the closest one available, the Durable Object that uses WebGPU will be instantiated where there are GPU resources available, which may not be on the same machine.</p></li></ul><p>Using Durable Objects instead of regular Workers allow us to address both of these issues.</p>
    <div>
      <h3>The WebGPU Hello World in Workers</h3>
      <a href="#the-webgpu-hello-world-in-workers">
        
      </a>
    </div>
    <p>Wrangler uses Miniflare 3, a <a href="/wrangler3/">fully-local simulator for Workers</a>, which in turn is powered by workerd. This means you can start experimenting and doing WebGPU code locally on your machine right now before we prepare things in our production environment.</p><p>Let’s get coding then.</p><p>Since Workers doesn't render graphics yet, we started with implementing the general-purpose GPU (GPGPU) APIs in the <a href="https://www.w3.org/TR/webgpu/">WebGPU specification</a>. In other words, we fully support the part of the API that the <a href="https://www.w3.org/TR/webgpu/#gpucomputepipeline">compute shaders and the compute pipeline</a> require, but we are not yet focused on fragment or vertex shaders used in rendering pipelines.</p><p>Here’s a typical “hello world” in WebGPU. This Durable Object script will output the name of the GPU device that workerd found in your machine to your console.</p>
            <pre><code>const adapter = await navigator.gpu.requestAdapter();
const adapterInfo = await adapter.requestAdapterInfo(["device"]);
console.log(adapterInfo.device);</code></pre>
            <p>A more interesting example, though, is a simple compute shader. In this case, we will fill a results buffer with an incrementing value taken from the iteration number via <code>global_invocation_id</code>.</p><p>For this, we need two buffers, one to store the results of the computations as they happen (<code>storageBuffer</code>) and another to copy the results at the end (<code>mappedBuffer</code>).</p><p>We then dispatch four workgroups, meaning that the increments can happen in parallel. This parallelism and programmability are two key reasons why compute shaders and GPUs provide an advantage for things like machine learning inference workloads. Other advantages are:</p><ul><li><p><b>Bandwidth</b> - GPUs have a very high memory bandwidth, up to 10-20x more than CPUs. This allows fast reading and writing of all the model parameters and data needed for inference.</p></li><li><p><b>Floating-point performance</b> - GPUs are optimized for high floating point operation throughput, which are used extensively in neural networks. They can deliver much higher <a href="https://www.tomshardware.com/reviews/gpu-hierarchy,4388.html">TFLOPs than CPUs</a>.</p></li></ul><p>Let’s look at the code:</p>
            <pre><code>// Create device and command encoder
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const encoder = device.createCommandEncoder();

// Storage buffer
const storageBuffer = device.createBuffer({
  size: 4 * Float32Array.BYTES_PER_ELEMENT, // 4 float32 values
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});

// Mapped buffer
const mappedBuffer = device.createBuffer({
  size: 4 * Float32Array.BYTES_PER_ELEMENT,
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});

// Create shader that writes incrementing numbers to storage buffer
const computeShaderCode = `
    @group(0) @binding(0)
    var&lt;storage, read_write&gt; result : array&lt;f32&gt;;

    @compute @workgroup_size(1)
    fn main(@builtin(global_invocation_id) gid : vec3&lt;u32&gt;) {
      result[gid.x] = f32(gid.x);
    }
`;

// Create compute pipeline
const computePipeline = device.createComputePipeline({
  layout: "auto",
  compute: {
    module: device.createShaderModule({ code: computeShaderCode }),
    entryPoint: "main",
  },
});

// Bind group
const bindGroup = device.createBindGroup({
  layout: computePipeline.getBindGroupLayout(0),
  entries: [{ binding: 0, resource: { buffer: storageBuffer } }],
});

// Dispatch compute work
const computePass = encoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroup);
computePass.dispatchWorkgroups(4);
computePass.end();

// Copy from storage to mapped buffer
encoder.copyBufferToBuffer(
  storageBuffer,
  0,
  mappedBuffer,
  0,
  4 * Float32Array.BYTES_PER_ELEMENT //mappedBuffer.size
);

// Submit and read back result
const gpuBuffer = encoder.finish();
device.queue.submit([gpuBuffer]);

await mappedBuffer.mapAsync(GPUMapMode.READ);
console.log(new Float32Array(mappedBuffer.getMappedRange()));
// [0, 1, 2, 3]</code></pre>
            <p>Now that we covered the basics of WebGPU and compute shaders, let's move to something more demanding. What if we could perform machine learning inference using Workers and GPUs?</p>
    <div>
      <h3>ONNX WebGPU demo</h3>
      <a href="#onnx-webgpu-demo">
        
      </a>
    </div>
    <p>The <a href="https://github.com/microsoft/onnxruntime">ONNX runtime</a> is a popular open-source cross-platform, high performance machine learning inferencing accelerator. <a href="https://github.com/webonnx/wonnx">Wonnx</a> is a GPU-accelerated version of the same engine, written in Rust, that can be compiled to WebAssembly and take advantage of WebGPU in the browser. We are going to run it in Workers using a combination of <a href="https://github.com/cloudflare/workers-rs">workers-rs</a>, our Rust bindings for Cloudflare Workers, and the workerd WebGPU APIs.</p><p>For this demo, we are using <a href="https://www.kdnuggets.com/2016/09/deep-learning-reading-group-squeezenet.html">SqueezeNet</a>. This small image classification model can run under lower resources but still achieves similar levels of accuracy on the <a href="https://en.wikipedia.org/wiki/ImageNet">ImageNet</a> image classification validation dataset as larger models like <a href="https://en.wikipedia.org/wiki/AlexNet">AlexNet</a>.</p><p>In essence, our worker will receive any uploaded image and attempt to classify it according to the 1000 ImageNet classes. Once ONNX runs the machine learning model using the GPU, it will return the list of classes with the highest probability scores. Let’s go step by step.</p><p>First we load the model from R2 into the GPU memory the first time the Durable Object is called:</p>
            <pre><code>#[durable_object]
pub struct Classifier {
    env: Env,
    session: Option&lt;wonnx::Session&gt;,
}

impl Classifier {
    async fn ensure_session(&amp;mut self) -&gt; Result&lt;()&gt; {
        match self.session {
            Some(_) =&gt; worker::console_log!("DO already has a session"),
            None =&gt; {
                // No session, so this should be the first request. In this case
                // we will fetch the model from R2, build a wonnx session, and
                // store it for subsequent requests.
                let model_bytes = fetch_model(&amp;self.env).await?;
                let session = wonnx::Session::from_bytes(&amp;model_bytes)
                    .await
                    .map_err(|err| err.to_string())?;
                worker::console_log!("session created in DO");
                self.session = Some(session);
            }
        };
        Ok(())
    }
}</code></pre>
            <p>This is only required once, when the Durable Object is instantiated. For subsequent requests, we retrieve the model input tensor, call the existing session for the inference, and return to the calling worker the result tensor converted to JSON:</p>
            <pre><code>        let request_data: ArrayBase&lt;OwnedRepr&lt;f32&gt;, Dim&lt;[usize; 4]&gt;&gt; =
            serde_json::from_str(&amp;req.text().await?)?;
        let mut input_data = HashMap::new();
        input_data.insert("data".to_string(), request_data.as_slice().unwrap().into());

        let result = self
            .session
            .as_ref()
            .unwrap() // we know the session exists
            .run(&amp;input_data)
            .await
            .map_err(|err| err.to_string())?;
...
        let probabilities: Vec&lt;f32&gt; = result
            .into_iter()
            .next()
            .ok_or("did not obtain a result tensor from session")?
            .1
            .try_into()
            .map_err(|err: TensorConversionError| err.to_string())?;

        let do_response = serde_json::to_string(&amp;probabilities)?;
        Response::ok(do_response)</code></pre>
            <p>On the Worker script itself, we load the uploaded image and pre-process it into a model input tensor:</p>
            <pre><code>    let image_file: worker::File = match req.form_data().await?.get("file") {
        Some(FormEntry::File(buf)) =&gt; buf,
        Some(_) =&gt; return Response::error("`file` part of POST form must be a file", 400),
        None =&gt; return Response::error("missing `file`", 400),
    };
    let image_content = image_file.bytes().await?;
    let image = load_image(&amp;image_content)?;</code></pre>
            <p>Finally, we call the GPU Durable Object, which runs the model and returns the most likely classes of our image:</p>
            <pre><code>    let probabilities = execute_gpu_do(image, stub).await?;
    let mut probabilities = probabilities.iter().enumerate().collect::&lt;Vec&lt;_&gt;&gt;();
    probabilities.sort_unstable_by(|a, b| b.1.partial_cmp(a.1).unwrap());
    Response::ok(LABELS[probabilities[0].0])</code></pre>
            <p>We packaged this demo in a public repository, so you can also run it. Make sure that you have a <a href="https://www.rust-lang.org/">Rust</a> compiler, <a href="https://nodejs.org/en">Node.js</a>, <a href="https://git-scm.com/">Git</a> and <a href="https://curl.se/">curl</a> installed, then clone the repository:</p>
            <pre><code>git clone https://github.com/cloudflare/workers-wonnx.git
cd workers-wonnx</code></pre>
            <p>Upload the model to the local R2 simulator:</p>
            <pre><code>npx wrangler@latest r2 object put model-bucket-dev/opt-squeeze.onnx --local --file models/opt-squeeze.onnx</code></pre>
            <p>And then run the Worker locally:</p>
            <pre><code>npx wrangler@latest dev</code></pre>
            <p>With the Worker running and waiting for requests you can then open another terminal window and upload one of the image examples in the same repository using curl:</p>
            <pre><code>&gt; curl -F "file=@images/pelican.jpeg" http://localhost:8787
n02051845 pelican</code></pre>
            <p>If everything goes according to plan the result of the curl command will be the most likely class of the image.</p>
    <div>
      <h3>Next steps and final words</h3>
      <a href="#next-steps-and-final-words">
        
      </a>
    </div>
    <p>Over the upcoming weeks, we will merge the workerd WebGPU code in the Cloudflare Workers production environment and make it available globally, on top of our growing GPU nodes fleet. We didn't do it earlier because that environment is subject to strict security and isolation requirements. For example, we can't break the <a href="https://developers.cloudflare.com/workers/learning/security-model/">security model</a> of our process sandbox and have V8 talking to the GPU hardware directly, that would be a problem; we must create a configuration where another process is closer to the GPU and use IPC (inter-process communication) to talk to it. Other things like managing resource allocation and billing are being sorted out.</p><p>For now, we wanted to get the good news out that we will support WebGPU in Cloudflare Workers and ensure that you can start playing and coding with it today and learn from it. WebGPU and general-purpose computing on GPUs is still in its early days. We presented a machine-learning demo, but we can imagine other applications taking advantage of this new feature, and we hope you can show us some of them.</p><p>As usual, you can talk to us on our <a href="https://discord.cloudflare.com/">Developers Discord</a> or the <a href="https://community.cloudflare.com/c/developers/39">Community forum</a>; the team will be listening. We are eager to hear from you and learn about what you're building.</p> ]]></content:encoded>
            <category><![CDATA[Birthday Week]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Standards]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <guid isPermaLink="false">4osLizDbNHndEk9BG23KFi</guid>
            <dc:creator>André Cruz</dc:creator>
            <dc:creator>Celso Martinho</dc:creator>
        </item>
        <item>
            <title><![CDATA[Privacy Gateway: a privacy preserving proxy built on Internet standards]]></title>
            <link>https://blog.cloudflare.com/building-privacy-into-internet-standards-and-how-to-make-your-app-more-private-today/</link>
            <pubDate>Thu, 27 Oct 2022 13:01:00 GMT</pubDate>
            <description><![CDATA[ Privacy Gateway enables privacy-forward applications to use Cloudflare as a trusted Relay, limiting which identifying information, including IP addresses, is visible to their infrastructure ]]></description>
            <content:encoded><![CDATA[ <p></p><p>If you’re running a privacy-oriented application or service on the Internet, your options to provably protect users’ privacy are limited. You can minimize logs and data collection but even then, at a network level, every HTTP request needs to come from <i>somewhere.</i> Information generated by HTTP requests, like users’ IP addresses and TLS fingerprints, can be sensitive especially when combined with application data.</p><p>Meaningful improvements to your users’ privacy require a change in how HTTP requests are sent from client devices to the server that runs your application logic. This was the motivation for Privacy Gateway: a service that relays encrypted HTTP requests and responses between a client and application server. With Privacy Gateway, Cloudflare knows where the request is coming from, but not what it contains, and applications can see what the request contains, but not where it comes from. <b>Neither Cloudflare nor the application server has the full picture</b>, improving end-user privacy.</p><p>We recently deployed Privacy Gateway for <a href="https://flo.health/">Flo Health Inc</a>., a leading female health app, for the launch of their <a href="https://www.theverge.com/2022/9/14/23351957/flo-period-tracker-privacy-anonymous-mode">Anonymous Mode</a>. With Privacy Gateway in place, all request data for Anonymous Mode users is encrypted between the app user and Flo, which prevents Flo from seeing the IP addresses of those users and Cloudflare from seeing the contents of that request data.</p><p>With Privacy Gateway in place, several other privacy-critical applications are possible:</p><ul><li><p>Browser developers can collect user telemetry in a privacy-respecting manner– what extensions are installed, what defaults a user might have changed — while removing what is still a potentially personal identifier (the IP address) from that data.</p></li><li><p>Users can visit a healthcare site to report a Covid-19 exposure without worrying that the site is tracking their IP address and/or location.</p></li><li><p>DNS resolvers can serve DNS queries without linking who made the request with what website they’re visiting – a pattern we’ve implemented with <a href="/oblivious-dns/">Oblivious DNS</a>.Privacy Gateway is based on <a href="https://datatracker.ietf.org/doc/draft-ietf-ohai-ohttp/">Oblivious HTTP (OHTTP), an emerging IETF standard</a> and is built upon standard <a href="https://datatracker.ietf.org/doc/html/rfc9180">hybrid public-key cryptography</a>.</p></li></ul>
    <div>
      <h2>How does it work?</h2>
      <a href="#how-does-it-work">
        
      </a>
    </div>
    <p>The main innovation in the Oblivious HTTP standard – beyond a basic proxy service – is that these messages are encrypted <i>to the application’s server</i>, such that Privacy Gateway learns nothing of the application data beyond the source and destination of each message.</p><p>Privacy Gateway enables application developers and platforms, especially those with strong privacy requirements, to build something that closely resembles a “<a href="https://en.wikipedia.org/wiki/Mix_network">Mixnet</a>”: an approach to obfuscating the source and destination of a message across a network. To that end, Privacy Gateway consists of three main components:</p><ol><li><p><b>Client:</b> the user’s device, or any client that’s configured to forward requests to Privacy Gateway.</p></li><li><p><b>Privacy Gateway:</b> a service operated by Cloudflare and designed to relay requests between the Client and the Gateway, without being able to observe the contents within.</p></li><li><p><b>Application server</b>: the origin or application web server responsible for decrypting requests from clients, and encrypting responses back.</p></li></ol><p>If you were to imagine request data as the contents of the envelope (a letter) and the IP address and request metadata as the address on the outside, with Privacy Gateway, Cloudflare is able to see the envelope’s address and safely forward it to its destination without being able to see what’s inside.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2sYdEzCvuyKU7h3wIUKju0/b845a85835291ffe7c9751355b9c6b5f/image4-9.png" />
            
            </figure><p>An Oblivious HTTP transaction using Privacy Gateway</p><p>In slightly more detail, the data flow is as follows:</p><ol><li><p>Client <a href="https://datatracker.ietf.org/doc/html/draft-thomson-http-oblivious-02#section-5.1">encapsulates an HTTP request</a> using the public key of the application server, and sends it to Privacy Gateway over an HTTPS connection.</p></li><li><p>Privacy Gateway forwards the request to the server over its own, separate HTTPS connection with the application server.</p></li><li><p>The application server  decapsulates the request, forwarding it to the target server which can produce the response.</p></li><li><p>The application server returns an <a href="https://datatracker.ietf.org/doc/html/draft-thomson-http-oblivious-02#section-5.2">encapsulated response</a> to Privacy Gateway, which then forwards the result to the client.As specified in the protocol, requests from the client to the server are encrypted using HPKE, a state-of-the-art standard for public key encryption – which you can read more about <a href="/hybrid-public-key-encryption/">here</a>. We’ve taken additional measures to ensure that OHTTP’s use of HPKE is secure by conducting a <a href="/stronger-than-a-promise-proving-oblivious-http-privacy-properties/">formal analysis of the protocol</a>, and we expect to publish a deeper analysis in the coming weeks.</p></li></ol>
    <div>
      <h2>How Privacy Gateway improves end-user privacy</h2>
      <a href="#how-privacy-gateway-improves-end-user-privacy">
        
      </a>
    </div>
    <p>This interaction offers two types of privacy, which we informally refer to as <i>request privacy</i> and <i>client privacy</i>.</p><p>Request privacy means that the application server does not learn information that would otherwise be revealed by an HTTP request, such as IP address, geolocation, TLS and HTTPS fingerprints, and so on. Because Privacy Gateway uses a separate HTTPS connection between itself and the application server, all of this per-request information revealed to the application server represents that of Privacy Gateway, not of the client. However, developers need to take care to not send personally identifying information in the contents of requests. If the request, once decapsulated, includes information like users’ email, phone number, or credit card info, for example, Privacy Gateway will not meaningfully improve privacy.</p><p>Client privacy is a stronger notion. Because Cloudflare and the application server are not colluding to share individual user’s data, from the server’s perspective, each individual transaction came from some unknown client behind Privacy Gateway. In other words, a properly configured Privacy Gateway deployment means that applications cannot link any two requests to the same client. In particular, with Privacy Gateway, privacy loves company. If there is only one end-user making use of Privacy Gateway, then it only provides request privacy (since the client IP address remains hidden from the Gateway). It would not provide client privacy, since the server would know that each request corresponds to the same, single client. Client privacy requires that there be many users of the system, so the application server cannot make this determination.</p><p>To better understand request and client privacy, consider the following HTTP request between a client and server:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6tEmiR5nN2XWpa1EqATvBY/e74edca6ab779548c43aaa357fd88578/image3-7.png" />
            
            </figure><p>Normal HTTP configuration with a client anonymity set of size 1</p><p>If a client connects directly to the server (or “Gateway” in OHTTP terms), the server is likely to see information about the client, including the IP address, TLS cipher used, and a degree of location data based on that IP address:</p>
            <pre><code>- ipAddress: 192.0.2.33 # the client’s real IP address
- ASN: 7922
- AS Organization: Comcast Cable
- tlsCipher: AEAD-CHACHA20-POLY1305-SHA256 # potentially unique
- tlsVersion: TLSv1.3
- Country: US
- Region: California
- City: Campbell</code></pre>
            <p>There’s plenty of sensitive information here that might be unique to the end-user. In other words, the connection offers neither request nor client privacy.</p><p>With Privacy Gateway, clients do not connect directly to the application server itself. Instead, they connect to Privacy Gateway, which in turn connects to the server. This means that the server only observes connections from Privacy Gateway, not individual connections from clients, yielding a different view:</p>
            <pre><code>- ipAddress: 104.16.5.5 # a Cloudflare IP
- ASN: 13335
- AS Organization: Cloudflare
- tlsCipher: ECDHE-ECDSA-AES128-GCM-SHA256 # shared across several clients
- tlsVersion: TLSv1.3
- Country: US
- Region: California
- City: Los Angeles</code></pre>
            
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/34pNp8QRamMWLMqBpT2Q05/4b400b029df3a7ad8821e7da69dd30b9/image1-18.png" />
            
            </figure><p>Privacy Gateway configuration with a client anonymity set of size k</p><p>This is request privacy. All information about the client’s location and identity are hidden from the application server. And all details about the application data are hidden from Privacy Gateway. For sensitive applications and protocols like DNS, keeping this metadata separate from the application data is an important step towards improving end-user privacy.</p><p>Moreover, applications should take care to not reveal sensitive, per-client information in their individual requests. Privacy Gateway cannot guarantee that applications do not send identifying info – such as email addresses, full names, etc – in request bodies, since it cannot observe plaintext application data. Applications which reveal user identifying information in requests may violate client privacy, but not request privacy. This is why – unlike our full <a href="/privacy-edge-making-building-privacy-first-apps-easier/">application-level Privacy Proxy</a> product – Privacy Gateway is <i>not</i> meant to be used as a generic proxy-based protocol for arbitrary applications and traffic. It is meant to be a special purpose protocol for sensitive applications, including DNS (as is evidenced by <a href="/oblivious-dns/">Oblivious DNS-over-HTTPS</a>), telemetry data, or generic API requests as discussed above.</p>
    <div>
      <h2>Integrating Privacy Gateway into your application</h2>
      <a href="#integrating-privacy-gateway-into-your-application">
        
      </a>
    </div>
    <p>Integrating with Privacy Gateway requires applications to implement the client and server side of the OHTTP protocol. Let’s walk through what this entails.</p>
    <div>
      <h3>Server Integration</h3>
      <a href="#server-integration">
        
      </a>
    </div>
    <p>The server-side part of the protocol is responsible for two basic tasks:</p><ol><li><p>Publishing a public key for request encapsulation; and</p></li><li><p>Decrypting encapsulated client requests, processing the resulting request, and encrypting the corresponding response.</p></li></ol><p>A <a href="https://ietf-wg-ohai.github.io/oblivious-http/draft-ietf-ohai-ohttp.html#name-key-configuration">public encapsulation key</a>, called a key configuration, consists of a key identifier (so the server can support multiple keys at once for rotation purposes), cryptographic algorithm identifiers for encryption and decryption, and a public key:</p>
            <pre><code>HPKE Symmetric Algorithms {
  HPKE KDF ID (16),
  HPKE AEAD ID (16),
}

OHTTP Key Config {
  Key Identifier (8),
  HPKE KEM ID (16),
  HPKE Public Key (Npk * 8),
  HPKE Symmetric Algorithms Length (16),
  HPKE Symmetric Algorithms (32..262140),
}</code></pre>
            <p>Clients need this public key to create their request, and there are lots of ways to do this. Servers could fix a public key and then bake it into their application, but this would require a software update to rotate the key. Alternatively, clients could discover the public key some other way. Many discovery mechanisms exist and vary based on your threat model – see <a href="https://datatracker.ietf.org/doc/html/draft-wood-key-consistency">this document</a> for more details. To start, a simple approach is to have clients fetch the public key directly from the server over some API. Below is a snippet of the API that our <a href="https://github.com/cloudflare/app-relay-gateway-go/blob/main/gateway.go#L116-L134">open source OHTTP server provides</a>:</p>
            <pre><code>func (s *GatewayResource) configHandler(w http.ResponseWriter, r *http.Request) {
	config, err := s.Gateway.Config(s.keyID)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	w.Write(config.Marshal())
}</code></pre>
            <p>Once public key generation and distribution is solved, the server then needs to handle encapsulated requests from clients. For each request, the server needs to decrypt the request, translate the plaintext to a corresponding HTTP request that can be resolved, and then encrypt the resulting response back to the client.</p><p>Open source OHTTP libraries typically offer functions for <a href="https://github.com/chris-wood/ohttp-go/blob/main/ohttp.go#L455">request decryption</a> and <a href="https://github.com/chris-wood/ohttp-go/blob/main/ohttp.go#L502-L541">response encryption</a>, whereas plaintext translation from <a href="https://datatracker.ietf.org/doc/html/rfc9292">binary HTTP</a> to an HTTP request is handled separately. For example, our open source server delegates this translation to a different library that is specific to how Go HTTP requests are represented in memory. In particular, the function to translate from a plaintext request to a <a href="https://pkg.go.dev/net/http#Request">Go HTTP request</a> is done with a function that has the following signature:</p>
            <pre><code>func UnmarshalBinaryRequest(data []byte) (*http.Request, error) {
	...
}</code></pre>
            <p>Conversely, translating a <a href="https://pkg.go.dev/net/http#Response">Go HTTP response</a> to a plaintext binary HTTP response message is done with a function that has the following signature:</p>
            <pre><code>type BinaryResponse http.Response

func (r *BinaryResponse) Marshal() ([]byte, error) {
	...
}</code></pre>
            <p>While there exist several open source libraries that one can use to implement OHTTP server support, we’ve packaged all of it up in our open source server implementation <a href="https://github.com/cloudflare/app-relay-gateway-go">available here</a>. It includes instructions for building, testing, and deploying to make it easy to get started.</p>
    <div>
      <h3>Client integration</h3>
      <a href="#client-integration">
        
      </a>
    </div>
    <p>Naturally, the client-side behavior of OHTTP mirrors that of the server. In particular, the client must:</p><ol><li><p>Discover or obtain the server public key; and</p></li><li><p>Encode and encrypt HTTP requests, send them to Privacy Gateway, and decrypt and decode the HTTP responses.</p></li></ol><p>Discovery of the server public key depends on the server’s chosen deployment model. For example, if the public key is available over an API, clients can simply fetch it directly:</p>
            <pre><code>$ curl https://server.example/ohttp-configs &gt; config.bin</code></pre>
            <p><a href="https://github.com/chris-wood/ohttp-go/blob/main/bhttp.go#L66">Encoding</a>, <a href="https://github.com/chris-wood/ohttp-go/blob/main/ohttp.go#L321">encrypting</a>, <a href="https://github.com/chris-wood/ohttp-go/blob/main/ohttp.go#L373">decrypting</a>, and decoding are again best handled by OHTTP libraries when available. With these functions available, building client support is rather straightforward. A trivial example Go client using the library functions linked above is as follows:</p>
            <pre><code>configEnc := ... // encoded public key
config, err := ohttp.UnmarshalPublicConfig(configEnc)
if err != nil {
	return err
}

request, err := http.NewRequest(http.MethodGet, "https://test.example/index.html", nil)
if err != nil {
	return err
}

binaryRequest := ohttp.BinaryRequest(*request)
encodedRequest, err := binaryRequest.Marshal()
if err != nil {
	return err
}

ohttpClient := ohttp.NewDefaultClient(config)
encapsulatedReq, reqContext, err := ohttpClient.EncapsulateRequest(encodedRequest)

relayRequest, err := http.NewRequest(http.MethodPost, "https://relay.example", bytes.NewReader(encapsulatedReq.Marshal()))
if err != nil {
	return err
}
relayRequest.Header.Set("Content-Type", "message/ohttp-req")

client := http.Client{}
relayResponse, err := client.Do(relayRequest)
if err != nil {
	return err
}
bodyBytes, err := ioutil.ReadAll(relayResponse.Body)
if err != nil {
	return err
}
encapsulatedResp, err := ohttp.UnmarshalEncapsulatedResponse(bodyBytes)
if err != nil {
	return err
}

receivedResp, err := reqContext.DecapsulateResponse(encapsulatedResp)
if err != nil {
	return err
}

response, err := ohttp.UnmarshalBinaryResponse(receivedResp)
if err != nil {
	return err
}

fmt.Println(response)</code></pre>
            <p>A standalone client like this isn’t likely very useful to you if you have an existing application. To help integration into your existing application, we created a <a href="https://github.com/cloudflare/app-relay-client-library">sample OHTTP client library</a> that’s compatible with iOS and macOS applications. Additionally, if there’s language or platform support you would like to see to help ease integration on either or the client or server side, please let us know!</p>
    <div>
      <h2>Interested?</h2>
      <a href="#interested">
        
      </a>
    </div>
    <p>Privacy Gateway is currently in early access – available to select privacy-oriented companies and partners. If you’re interested, <a href="https://www.cloudflare.com/lp/privacy-edge/">please get in touch</a>.</p> ]]></content:encoded>
            <category><![CDATA[Privacy]]></category>
            <category><![CDATA[Protocols]]></category>
            <category><![CDATA[Standards]]></category>
            <guid isPermaLink="false">4KXLduwaLrRcnGTcApXBpH</guid>
            <dc:creator>Mari Galicer</dc:creator>
            <dc:creator>Christopher Wood</dc:creator>
        </item>
        <item>
            <title><![CDATA[HPKE: Standardizing public-key encryption (finally!)]]></title>
            <link>https://blog.cloudflare.com/hybrid-public-key-encryption/</link>
            <pubDate>Thu, 24 Feb 2022 23:12:36 GMT</pubDate>
            <description><![CDATA[ HPKE (RFC 9180) was made to be simple, reusable, and future-proof by building upon knowledge from prior PKE schemes and software implementations. This article provides an overview of this new standard, going back to discuss its motivation, design goals, and development process ]]></description>
            <content:encoded><![CDATA[ <p>For the last three years, the <a href="https://irtf.org/cfrg">Crypto Forum Research Group</a> of the <a href="https://irtf.org/">Internet Research Task Force (IRTF)</a> has been working on specifying the next generation of (hybrid) public-key encryption (PKE) for Internet protocols and applications. The result is Hybrid Public Key Encryption (HPKE), published today as <a href="https://www.rfc-editor.org/rfc/rfc9180.html">RFC 9180</a>.</p><p>HPKE was made to be simple, reusable, and future-proof by building upon knowledge from prior PKE schemes and software implementations. It is already in use in a large assortment of emerging Internet standards, including TLS <a href="https://datatracker.ietf.org/doc/draft-ietf-tls-esni/">Encrypted Client Hello</a> and <a href="https://datatracker.ietf.org/doc/draft-pauly-dprive-oblivious-doh/">Oblivious DNS-over-HTTPS</a>, and has a large assortment of interoperable implementations, including one in <a href="https://github.com/cloudflare/circl/tree/master/hpke">CIRCL</a>. This article provides an overview of this new standard, going back to discuss its motivation, design goals, and development process.</p>
    <div>
      <h3>A primer on public-key encryption</h3>
      <a href="#a-primer-on-public-key-encryption">
        
      </a>
    </div>
    <p>Public-key cryptography is decades old, with its roots going back to the seminal work of Diffie and Hellman in 1976, entitled “<a href="https://ee.stanford.edu/~hellman/publications/24.pdf">New Directions in Cryptography</a>.” Their proposal – today called Diffie-Hellman key exchange – was a breakthrough. It allowed one to transform small secrets into big secrets for cryptographic applications and protocols. For example, one can bootstrap a secure channel for exchanging messages with confidentiality and integrity using a key exchange protocol.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/49zWFnGoaVQPUcKjDT52uO/09610fee57bd627cb23faae14bc05ca4/Screen-Shot-2022-02-24-at-11.09.26-AM.png" />
            
            </figure><p>Unauthenticated Diffie-Hellman key exchange</p><p>In this example, Sender and Receiver exchange freshly generated public keys with each other, and then combine their own secret key with their peer’s public key. Algebraically, this yields the same value \(g^{xy} = (g^x)^y = (g^y)^x\). Both parties can then use this as a <i>shared secret</i> for performing other tasks, such as encrypting messages to and from one another.</p><p>The <a href="https://datatracker.ietf.org/doc/html/rfc8446">Transport Layer Security</a> (TLS) protocol is one such application of this concept. Shortly after Diffie-Hellman was unveiled to the world, RSA came into the fold. The RSA cryptosystem is another public key algorithm that has been used to build digital signature schemes, PKE algorithms, and key transport protocols. A key transport protocol is similar to a key exchange algorithm in that the sender, Alice, generates a random symmetric key and then encrypts it under the receiver’s public key. Upon successful decryption, both parties then share this secret key. (This fundamental technique, known as static RSA, was used pervasively in the context of TLS. See <a href="/rfc-8446-aka-tls-1-3/">this post</a> for details about this old technique in TLS 1.2 and prior versions.)</p><p>At a high level, PKE between a sender and receiver is a protocol for encrypting messages under the receiver’s public key. One way to do this is via a so-called non-interactive key exchange protocol.</p><p>To illustrate how this might work, let \(g^y\) be the receiver’s public key, and let \(m\) be a message that one wants to send to this receiver. The flow looks like this:</p><ol><li><p>The sender generates a fresh private and public key pair, \((x, g^x)\).</p></li><li><p>The sender computes \(g^{xy} = (g^y)^x\), which can be done without involvement from the receiver, that is, non-interactively.</p></li><li><p>The sender then uses this shared secret to derive an encryption key, and uses this key to encrypt m.</p></li><li><p>The sender packages up \(g^x\) and the encryption of \(m\), and sends both to the receiver.</p></li></ol><p>The general paradigm here is called "hybrid public-key encryption" because it combines a non-interactive key exchange based on public-key cryptography for establishing a shared secret, and a symmetric encryption scheme for the actual encryption. To decrypt \(m\), the receiver computes the same shared secret \(g^{xy} = (g^x)^y\), derives the same encryption key, and then decrypts the ciphertext.</p><p>Conceptually, PKE of this form is quite simple. General designs of this form date back for many years and include the <a href="https://www.cs.ucdavis.edu/~rogaway/papers/dhies.pdf">Diffie-Hellman Integrated Encryption System</a> (DHIES) and ElGamal encryption. However, despite this apparent simplicity, there are numerous subtle design decisions one has to make in designing this type of protocol, including:</p><ul><li><p>What type of key exchange protocol should be used for computing the shared secret? Should this protocol be based on modern elliptic curve groups like Curve25519? Should it support future post-quantum algorithms?</p></li><li><p>How should encryption keys be derived? Are there other keys that should be derived? How should additional application information be included in the encryption key derivation, if at all?</p></li><li><p>What type of encryption algorithm should be used? What types of messages should be encrypted?</p></li><li><p>How should sender and receiver encode and exchange public keys?</p></li></ul><p>These and other questions are important for a protocol, since they are required for interoperability. That is, senders and receivers should be able to communicate without having to use the same source code.</p><p>There have been a number of efforts in the past to standardize PKE, most of which focus on elliptic curve cryptography. Some examples of past standards include: ANSI X9.63 (ECIES), IEEE 1363a, ISO/IEC 18033-2, and SECG SEC 1.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/85SKzkEqGOEuyw4brVcak/fe2a769b6c8210d6851f265e46dec90a/Screen-Shot-2022-02-24-at-11.09.53-AM.png" />
            
            </figure><p>Timeline of related standards and software</p><p>A paper by <a href="https://ieeexplore.ieee.org/abstract/document/5604194/">Martinez et al.</a> provides a thorough and technical comparison of these different standards. The key points are that all these existing schemes have shortcomings. They either rely on outdated or not-commonly-used primitives such as <a href="https://en.wikipedia.org/wiki/RIPEMD">RIPEMD</a> and CMAC-AES, lack accommodations for moving to modern primitives (e.g., <a href="https://datatracker.ietf.org/doc/html/rfc5116">AEAD</a> algorithms), lack proofs of <a href="https://link.springer.com/chapter/10.1007/BFb0055718">IND-CCA2</a> security, or, importantly, fail to provide test vectors and interoperable implementations.</p><p>The lack of a single standard for public-key encryption has led to inconsistent and often non-interoperable support across libraries. In particular, hybrid PKE implementation support is fractured across the community, ranging from the hugely popular and simple-to-use <a href="https://nacl.cr.yp.to/box.html">NaCl box</a> and <a href="https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes">libsodium box seal</a> implementations based on modern algorithm variants like X-SalsaPoly1305 for authenticated encryption, to <a href="https://www.bouncycastle.org/specifications.html">BouncyCastle</a> implementations based on “classical” algorithms like AES and elliptic curves.</p><p>Despite the lack of a single standard, this hasn’t stopped the adoption of ECIES instantiations for widespread and critical applications. For example, the Apple and Google <a href="https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ENPA_White_Paper.pdf">Exposure Notification Privacy-preserving Analytics</a> (ENPA) platform uses ECIES for public-key encryption.</p><p>When designing protocols and applications that need a simple, reusable, and agile abstraction for public-key encryption, existing standards are not fit for purpose. That’s where HPKE comes into play.</p>
    <div>
      <h3>Construction and design goals</h3>
      <a href="#construction-and-design-goals">
        
      </a>
    </div>
    <p>HPKE is a public-key encryption construction that is designed from the outset to be simple, reusable, and future-proof. It lets a sender encrypt arbitrary-length messages under a receiver’s public key, as shown below. You can try this out in the browser at <a href="https://www.franziskuskiefer.de/p/tldr-hybrid-public-key-encryption/">Franziskus Kiefer’s blog post on HPKE</a>!</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2QjgXQ7cSPcN5kLUpdpYEO/ba5071877cd0b2b6168d2c4e9493ea52/image5-2.png" />
            
            </figure><p>HPKE overview</p><p>HPKE is built in stages. It starts with a Key Encapsulation Mechanism (KEM), which is similar to the key transport protocol described earlier and, in fact, can be constructed from the Diffie-Hellman key agreement protocol. A KEM has two algorithms: Encapsulation and Decapsulation, or Encap and Decap for short. The Encap algorithm creates a symmetric secret and wraps it for a public key such that only the holder of the corresponding private key can unwrap it. An attacker knowing this encapsulated key cannot recover even a single bit of the shared secret. Decap takes the encapsulated key and the private key associated with the public key, and computes the original shared secret. From this shared secret, HPKE computes a series of derived keys that are then used to encrypt and authenticate plaintext messages between sender and receiver.</p><p>This simple construction was driven by several high-level design goals and principles. We will discuss these goals and how they were met below.</p>
    <div>
      <h3>Algorithm agility</h3>
      <a href="#algorithm-agility">
        
      </a>
    </div>
    <p>Different applications, protocols, and deployments have different constraints, and locking any single use case into a specific (set of) algorithm(s) would be overly restrictive. For example, some applications may wish to use post-quantum algorithms when available, whereas others may wish to use different authenticated encryption algorithms for symmetric-key encryption. To accomplish this goal, HPKE is designed as a composition of a Key Encapsulation Mechanism (KEM), Key Derivation Function (KDF), and Authenticated Encryption Algorithm (AEAD). Any combination of the three algorithms yields a valid instantiation of HPKE, subject to certain security constraints about the choice of algorithm.</p><p>One important point worth noting here is that HPKE is not a <i>protocol</i>, and therefore does nothing to ensure that sender and receiver agree on the HPKE ciphersuite or shared context information. Applications and protocols that use HPKE are responsible for choosing or negotiating a specific HPKE ciphersuite that fits their purpose. This allows applications to be opinionated about their choice of algorithms to simplify implementation and analysis, as is common with protocols like <a href="https://www.wireguard.com/">WireGuard</a>, or be flexible enough to support choice and agility, as is the approach taken with TLS.</p>
    <div>
      <h3>Authentication modes</h3>
      <a href="#authentication-modes">
        
      </a>
    </div>
    <p>At a high level, public-key encryption ensures that only the holder of the private key can decrypt messages encrypted for the corresponding public key (being able to decrypt the message is an implicit authentication of the receiver.) However, there are other ways in which applications may wish to authenticate messages from sender to receiver. For example, if both parties have a pre-shared key, they may wish to ensure that both can demonstrate possession of this pre-shared key as well. It may also be desirable for senders to demonstrate knowledge of their own private key in order for recipients to decrypt the message (this functionally is similar to signing an encryption, but has some <a href="https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hpke-12#section-9.1.1">subtle and important differences</a>).</p><p>To support these various use cases, HPKE admits different modes of authentication, allowing various combinations of pre-shared key and sender private key authentication. The additional private key contributes to the shared secret between the sender and receiver, and the pre-shared key contributes to the derivation of the application data encryption secrets. This process is referred to as the “key schedule”, and a simplified version of it is shown below.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3kF11CKLHDthJSduTAD10i/d47cc6df3c62d35970674fcd6e25df9e/image2-11.png" />
            
            </figure><p>Simplified HPKE key schedule</p><p>These modes come at a price, however: not all KEM algorithms will work with all authentication modes. For example, for most post-quantum KEM algorithms there isn’t a private key authentication variant known.</p>
    <div>
      <h3>Reusability</h3>
      <a href="#reusability">
        
      </a>
    </div>
    <p>The core of HPKE’s construction is its key schedule. It allows secrets produced and shared with KEMs and pre-shared keys to be mixed together to produce additional shared secrets between sender and receiver for performing authenticated encryption and decryption. HPKE allows applications to build on this key schedule without using the corresponding AEAD functionality, for example, by <a href="https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hpke-12#section-5.3">exporting a shared application-specific secret</a>. Using HPKE in an “export-only” fashion allows applications to use other, non-standard AEAD algorithms for encryption, should that be desired. It also allows applications to use a KEM different from those specified in the standard, as is done in the proposed <a href="https://claucece.github.io/draft-celi-wiggers-tls-authkem/draft-celi-wiggers-tls-authkem.html">TLS AuthKEM draft</a>.</p>
    <div>
      <h3>Interface simplicity</h3>
      <a href="#interface-simplicity">
        
      </a>
    </div>
    <p>HPKE hides the complexity of message encryption from callers. Encrypting a message with additional authenticated data from sender to receiver for their public key is as simple as the following two calls:</p>
            <pre><code>// Create an HPKE context to send messages to the receiver
encapsulatedKey, senderContext = SetupBaseS(receiverPublicKey, ”shared application info”)

// AEAD encrypt the message using the context
ciphertext = senderContext.Seal(aad, message)</code></pre>
            <p>In fact, many implementations are likely to offer a simplified <a href="https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hpke-12#section-6">“single-shot” interface</a> that does context creation and message encryption with one function call.</p><p>Notice that this interface does not expose anything like nonce ("number used once") or sequence numbers to the callers. The HPKE context manages nonce and sequence numbers internally, which means the application is responsible for message ordering and delivery. This was an important design decision done to hedge against key and nonce reuse, <a href="https://fahrplan.events.ccc.de/congress/2010/Fahrplan/events/4087.en.html">which</a> <a href="https://link.springer.com/chapter/10.1007/978-3-662-45611-8_14">can</a> <a href="https://eprint.iacr.org/2014/161">be</a> <a href="https://eprint.iacr.org/2019/023.pdf">catastrophic</a> for <a href="https://eprint.iacr.org/2020/615">security</a>.</p><p>Consider what would be necessary if HPKE delegated nonce management to the application. The sending application using HPKE would need to communicate the nonce along with each ciphertext value for the receiver to successfully decrypt the message. If this nonce was ever reused, then security of the <a href="https://eprint.iacr.org/2016/475">AEAD may fall apart</a>. Thus, a sending application would necessarily need some way to ensure that nonces were never reused. Moreover, by sending the nonce to the receiver, the application is effectively implementing a message sequencer. The application could just as easily implement and use this sequencer to ensure in-order message delivery and processing. Thus, at the end of the day, exposing the nonce seemed both harmful and, ultimately, redundant.</p>
    <div>
      <h3>Wire format</h3>
      <a href="#wire-format">
        
      </a>
    </div>
    <p>Another hallmark of HPKE is that all messages that do not contain application data are fixed length. This means that serializing and deserializing HPKE messages is trivial and there is no room for application choice. In contrast, some implementations of hybrid PKE deferred choice of wire format details, such as whether to use elliptic curve point compression, to applications. HPKE handles this under the KEM abstraction.</p>
    <div>
      <h3>Development process</h3>
      <a href="#development-process">
        
      </a>
    </div>
    <p>HPKE is the result of a three-year development cycle between industry practitioners, protocol designers, and academic cryptographers. In particular, HPKE built upon prior art relating to public-key encryption, iterated on a design and specification in a tight specification, implementation, experimentation, and analysis loop, with an ultimate goal towards real world use.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/11GwbhP4mtylFViu3VGoqP/580f50018acc54ba7d8fef892f339bf4/image3-15.png" />
            
            </figure><p>HPKE development process</p><p>This process isn’t new. TLS 1.3 and QUIC famously demonstrated this as an effective way of producing high quality technical specifications that are maximally useful for their consumers.</p><p>One particular point worth highlighting in this process is the value of interoperability and analysis. From the very first draft, interop between multiple, independent implementations was a goal. And since then, every revision was carefully checked by multiple library maintainers for soundness and correctness. This helped catch a number of mistakes and improved overall clarity of the technical specification.</p><p>From a formal analysis perspective, HPKE brought novel work to the community. Unlike protocol design efforts like those around TLS and QUIC, HPKE was simpler, but still came with plenty of sharp edges. As a new cryptographic construction, analysis was needed to ensure that it was sound and, importantly, to understand its limits. This analysis led to a number of important contributions to the community, including a <a href="https://eprint.iacr.org/2020/1499.pdf">formal analysis of HPKE</a>, new understanding of the <a href="https://dl.acm.org/doi/abs/10.1145/3460120.3484814">limits of ChaChaPoly1305 in a multi-user security setting</a>, as well as a new CFRG specification documenting <a href="https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/">limits for AEAD algorithms</a>. For more information about the analysis effort that went into HPKE, check out this <a href="https://www.benjaminlipp.de/p/hpke-cryptographic-standard/">companion blog</a> by Benjamin Lipp, an HPKE co-author.</p>
    <div>
      <h3>HPKE’s future</h3>
      <a href="#hpkes-future">
        
      </a>
    </div>
    <p>While HPKE may be a new standard, it has already seen a tremendous amount of adoption in the industry. As mentioned earlier, it’s an essential part of the TLS Encrypted Client Hello and Oblivious DoH standards, both of which are deployed protocols on the Internet today. Looking ahead, it’s also been integrated as part of the emerging <a href="https://datatracker.ietf.org/doc/charter-ietf-ohai/">Oblivious HTTP</a>, <a href="https://datatracker.ietf.org/wg/mls/about/">Message Layer Security</a>, and <a href="https://www.ietf.org/id/draft-gpew-priv-ppm-00.html">Privacy Preserving Measurement</a> standards. HPKE’s hallmark is its generic construction that lets it adapt to a wide variety of application requirements. If an application needs public-key encryption with a <a href="https://eprint.iacr.org/2020/1153.pdf">key-committing AEAD</a>, one can simply instantiate HPKE using a key-committing AEAD.</p><p>Moreover, there exists a huge assortment of interoperable implementations built on popular cryptographic libraries, including <a href="https://github.com/cisco/mlspp/tree/main/lib/hpke">OpenSSL</a>, <a href="https://boringssl.googlesource.com/boringssl/+/refs/heads/master/include/openssl/hpke.h">BoringSSL</a>, <a href="https://hg.mozilla.org/projects/nss/file/tip/lib/pk11wrap">NSS</a>, and <a href="https://github.com/cloudflare/circl/tree/master/hpke">CIRCL</a>. There are also formally verified implementations in <a href="https://www.franziskuskiefer.de/p/an-executable-hpke-specification/">hacspec and F*</a>; check out this <a href="https://tech.cryspen.com/hpke-spec">blog post</a> for more details. The complete set of known implementations is tracked <a href="https://github.com/cfrg/draft-irtf-cfrg-hpke#existing-hpke-implementations">here</a>. More implementations will undoubtedly follow in their footsteps.</p><p>HPKE is ready for prime time. I look forward to seeing how it simplifies protocol design and development in the future. Welcome, <a href="https://www.rfc-editor.org/rfc/rfc9180.html">RFC 9180</a>.</p> ]]></content:encoded>
            <category><![CDATA[Research]]></category>
            <category><![CDATA[IETF]]></category>
            <category><![CDATA[Cryptography]]></category>
            <category><![CDATA[Standards]]></category>
            <guid isPermaLink="false">2y7fDoXJoE5tJvjDMO7kap</guid>
            <dc:creator>Christopher Wood</dc:creator>
        </item>
        <item>
            <title><![CDATA[Cloudflare and the IETF]]></title>
            <link>https://blog.cloudflare.com/cloudflare-and-the-ietf/</link>
            <pubDate>Wed, 13 Oct 2021 12:59:37 GMT</pubDate>
            <description><![CDATA[ Cloudflare helps build a better Internet through collaboration on open and interoperable standards. This post will describe how Cloudflare contributes to the standardization process to enable incremental innovation and drive long-term architectural change. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>The Internet, far from being just a series of tubes, is a huge, incredibly complex, decentralized system. Every action and interaction in the system is enabled by a complicated mass of protocols woven together to accomplish their task, each handing off to the next like trapeze artists high above a virtual circus ring. Stop to think about details, and it is a marvel.</p><p>Consider one of the simplest tasks enabled by the Internet: Sending a message from sender to receiver.</p><p>The location (address) of a receiver is discovered using <a href="https://www.cloudflare.com/learning/dns/what-is-dns/">DNS</a>, a connection between sender and receiver is established using a transport protocol like TCP, and (hopefully!) secured with a protocol like TLS. The sender's message is encoded in a format that the receiver can recognize and parse, like HTTP, because the two disparate parties need a common language to communicate. Then, ultimately, the message is sent and carried in an IP datagram that is forwarded from sender to receiver based on routes established with BGP.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5Z79TEfHR8kGEqa8qMWBCQ/eecb98d60c7bbcbf5baae72ee10d8357/image1-35.png" />
            
            </figure><p>Even an explanation this dense is laughably oversimplified. For example, the four protocols listed are just the start, and ignore many others with acronyms of their own. The truth is that things are complicated. And because things are complicated, how these protocols and systems interact and influence the user experience on the Internet is complicated. Extra round trips to establish a secure connection increase the amount of time before useful work is done, harming user performance. The use of unauthenticated or unencrypted protocols reveals potentially sensitive information to the network or, worse, to malicious entities, which harms user security and privacy. And finally, consolidation and centralization — seemingly a prerequisite for reducing costs and protecting against attacks — makes it challenging to provide high availability even for essential services. (What happens when that one system goes down or is otherwise unavailable, or to extend our earlier metaphor, when a trapeze isn’t there to catch?)</p><p>These four properties — performance, security, privacy, and availability — are crucial to the Internet. At Cloudflare, and especially in the Cloudflare Research team, where we use all these various protocols, we're committed to improving them at every layer in the stack. We work on problems as diverse as <a href="https://www.cloudflare.com/network-security/">helping network security</a> and privacy with <a href="https://datatracker.ietf.org/doc/html/rfc8446">TLS 1.3</a> and <a href="https://datatracker.ietf.org/doc/html/rfc9000">QUIC,</a> improving DNS privacy via <a href="/oblivious-dns/">Oblivious DNS-over-HTTPS</a>, reducing end-user CAPTCHA annoyances with Privacy Pass and <a href="/introducing-cryptographic-attestation-of-personhood/">Cryptographic Attestation of Personhood (CAP)</a>, performing Internet-wide measurements to understand how things work in the real world, and much, much more.</p><p>Above all else, these projects are meant to do one thing: focus beyond the horizon to help build a better Internet. We do that by developing, advocating, and advancing open standards for the many protocols in use on the Internet, all backed by implementation, experimentation, and analysis.</p>
    <div>
      <h3>Standards</h3>
      <a href="#standards">
        
      </a>
    </div>
    <p>The Internet is a network of interconnected autonomous networks. Computers attached to these networks have to be able to route messages to each other. However, even if we can send messages back and forth across the Internet, much like the storied Tower of Babel, to achieve anything those computers have to use a common language, a lingua franca, so to speak. And for the Internet, standards are that common language.</p><p>Many of the parts of the Internet that Cloudflare is interested in are standardized by the IETF, which is a standards development organization responsible for producing technical specifications for the Internet's most important protocols, including IP, BGP, DNS, TCP, TLS, QUIC, HTTP, and so on. The <a href="https://www.ietf.org/about/mission/">IETF's mission</a> is:</p><p>&gt; to make the Internet work better by producing high-quality, relevant technical documents that influence the way people design, use, and manage the Internet.</p><p>Our individual contributions to the IETF help further this mission, especially given our role on the Internet. We can only do so much on our own to improve the end-user experience. So, through standards, we engage with those who use, manage, and operate the Internet to achieve three simple goals that lead to a better Internet:</p><p>1. Incrementally improve existing and deployed protocols with innovative solutions;</p><p>2. Provide holistic solutions to long-standing architectural problems and enable new use cases; and</p><p>3. Identify key problems and help specify reusable, extensible, easy-to-implement abstractions for solving them.</p><p>Below, we’ll give an example of how we helped achieve each goal, touching on a number of important technical specifications produced in recent years, including DNS-over-HTTPS, QUIC, and (the still work-in-progress) TLS Encrypted Client Hello.</p>
    <div>
      <h3>Incremental innovation: metadata privacy with DoH and ECH</h3>
      <a href="#incremental-innovation-metadata-privacy-with-doh-and-ech">
        
      </a>
    </div>
    <p>The Internet is not only complicated — it is leaky. Metadata seeps like toxic waste from nearly every protocol in use, from DNS to TLS, and even to HTTP at the application layer.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1t1ZVnKH9ZGQnKCgx6I8Pr/ada911c196fb971b19b8a4a3f7767362/image6-14.png" />
            
            </figure><p>One critically important piece of metadata that still leaks today is the name of the server that clients connect to. When a client opens a connection to a server, it reveals the name and identity of that server in many places, including DNS, TLS, and even sometimes at the IP layer (if the destination IP address is unique to that server). Linking client identity (IP address) to target server names enables third parties to build a profile of per-user behavior without end-user consent. The result is a set of protocols that does not respect end-user privacy.</p><p>Fortunately, it’s possible to incrementally address this problem without regressing security. For years, Cloudflare has been working with the standards community to plug all of these individual leaks through separate specialized protocols:</p><ul><li><p><a href="https://datatracker.ietf.org/doc/html/rfc8484">DNS-over-HTTPS</a> encrypts DNS queries between clients and recursive resolvers, ensuring only clients and trusted recursive resolvers see plaintext DNS traffic.</p></li><li><p><a href="https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-13">TLS Encrypted Client Hello</a> encrypts metadata in the TLS handshake, ensuring only the client and authoritative TLS server see sensitive TLS information.</p></li></ul><p>These protocols impose a barrier between the client and server and everyone else. However, neither of them prevent the server from building per-user profiles. Servers can track users via one critically important piece of information: the client IP address. Fortunately, for the overwhelming majority of cases, the IP address is not essential for providing a service. For example, DNS recursive resolvers do not need the full client IP address to provide accurate answers, as is evidenced by the <a href="https://datatracker.ietf.org/doc/html/rfc7871">EDNS(0) Client Subnet</a> extension. To further reduce information exposure on the web, we helped push further with two more incremental improvements:</p><ul><li><p><a href="https://datatracker.ietf.org/doc/html/draft-pauly-dprive-oblivious-doh-07">Oblivious DNS-over-HTTPS</a> (ODoH) uses cryptography and network proxies to break linkability between client identity (IP address) and DNS traffic, ensuring that recursive resolvers have only the minimal amount of information to provide DNS answers -- the queries themselves, without any per-client information.</p></li><li><p><a href="https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-04">MASQUE</a> is standardizing techniques for proxying UDP and IP protocols over QUIC connections, similar to the existing <a href="https://www.rfc-editor.org/rfc/rfc7231.html#section-4.3.6">HTTP CONNECT</a> method for TCP-based protocols. Generally, the CONNECT method allows clients to use services without revealing any client identity (IP address).</p></li></ul><p>While each of these protocols may seem only an incremental improvement over what we have today, together, they raise many possibilities for the future of the Internet. Are DoH and ECH sufficient for end-user privacy, or are technologies like ODoH and MASQUE necessary? How do proxy technologies like MASQUE complement or even subsume protocols like ODoH and ECH? These are questions the Cloudflare Research team strives to answer through experimentation, analysis, and deployment together with other stakeholders on the Internet through the IETF. And we could not ask the questions without first laying the groundwork.</p>
    <div>
      <h3>Architectural advancement: QUIC and HTTP/3</h3>
      <a href="#architectural-advancement-quic-and-http-3">
        
      </a>
    </div>
    <p><a href="https://quicwg.org">QUIC</a> and <a href="https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34">HTTP/3</a> are transformative technologies. Whilst the TLS handshake forms the heart of QUIC’s security model, QUIC is an improvement beyond TLS over TCP, in many respects, including more encryption (privacy), better protection against active attacks and ossification at the network layer, fewer round trips to establish a secure connection, and generally better security properties. QUIC and HTTP/3 give us a clean slate for future innovation.</p><p>Perhaps one of QUIC’s most important contributions is that it challenges and even breaks many established conventions and norms used on the Internet. For example, the antiquated socket API for networking, which treats the network connection as an in-order bit pipe is no longer appropriate for modern applications and developers. Modern networking APIs such as Apple’s <a href="https://developer.apple.com/documentation/network">Network.framework</a> provide high-level interfaces that take advantage of the new transport features provided by QUIC. Applications using this or even higher-level HTTP abstractions can take advantage of the many security, privacy, and performance improvements of QUIC and HTTP/3 today with minimal code changes, and without being constrained by sockets and their inherent limitations.</p><p>Another salient feature of QUIC is its wire format. Nearly every bit of every QUIC packet is encrypted and authenticated between sender and receiver. And within a QUIC packet, individual frames can be rearranged, repackaged, and otherwise transformed by the sender.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4qpdpgnX8A8M6iHvf0ECWP/aae602a63abed5400ffa431b4ce3cdce/image2-22.png" />
            
            </figure><p>Together, these are powerful tools to help mitigate future network ossification and enable continued extensibility. (TLS’s wire format ultimately led to the <a href="https://datatracker.ietf.org/doc/html/rfc8446#appendix-D.4">middlebox compatibility mode</a> for TLS 1.3 due to the many middlebox ossification problems that were encountered during early deployment tests.)</p><p>Exercising these features of QUIC is important for the <a href="https://datatracker.ietf.org/doc/html/draft-iab-use-it-or-lose-it-03">long-term health</a> of the protocol and applications built on top of it. Indeed, this sort of extensibility is what enables innovation.</p><p>In fact, we've already seen a flurry of new work based on QUIC: extensions to enable multipath QUIC, different congestion control approaches, and ways to carry data unreliably in the DATAGRAM frame.</p><p>Beyond functional extensions, we’ve also seen a number of new use cases emerge as a result of QUIC. DNS-over-QUIC is an upcoming proposal that complements DNS-over-TLS for recursive to authoritative DNS query protection. As mentioned above, MASQUE is a working group focused on standardizing methods for proxying arbitrary UDP and IP protocols over QUIC connections, enabling a number of fascinating solutions and unlocking the future of proxy and VPN technologies. In the context of the web, the WebTransport working group is standardizing methods to use QUIC as a “supercharged WebSocket” for transporting data efficiently between client and server while also depending on the WebPKI for security.</p><p>By definition, these extensions are nowhere near complete. The future of the Internet with QUIC is sure to be a fascinating adventure.</p>
    <div>
      <h3>Specifying abstractions: Cryptographic algorithms and protocol design</h3>
      <a href="#specifying-abstractions-cryptographic-algorithms-and-protocol-design">
        
      </a>
    </div>
    <p>Standards allow us to build abstractions. An ideal standard is one that is usable in many contexts and contains all the information a sufficiently skilled engineer needs to build a compliant implementation that successfully interoperates with other independent implementations. Writing a new standard is sort of like creating a new Lego brick. Creating a new Lego brick allows us to build things that we couldn’t have built before. For example, one new “brick” that’s nearly finished (as of this writing) is <a href="https://www.ietf.org/archive/id/draft-irtf-cfrg-hpke-12.html">Hybrid Public Key Encryption (HPKE)</a>. HPKE allows us to efficiently encrypt arbitrary plaintexts under the recipient’s public key.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5eWRfVLYtCcnUohsI2X8SE/48ccbddb899b98e65baea220bd7c06f6/image4-21.png" />
            
            </figure><p>Mixing asymmetric and symmetric cryptography for efficiency is a common technique that has been used for many years in all sorts of protocols, from TLS to <a href="https://en.wikipedia.org/wiki/Pretty_Good_Privacy">PGP</a>. However, each of these applications has come up with their own design, each with its own security properties. HPKE is intended to be a single, standard, interoperable version of this technique that turns this complex and technical corner of protocol design into an easy-to-use black box. The standard has undergone extensive analysis by cryptographers throughout its development and has numerous implementations available. The end result is a simple abstraction that protocol designers can include without having to consider how it works under-the-hood. In fact, HPKE is already a dependency for a number of other draft protocols in the IETF, such as <a href="https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-13">TLS Encrypted Client Hello</a>, <a href="https://datatracker.ietf.org/doc/html/draft-pauly-dprive-oblivious-doh-07">Oblivious DNS-over-HTTPS</a>, and <a href="https://datatracker.ietf.org/doc/html/draft-ietf-mls-architecture-07.html">Message Layer Security</a>.</p>
    <div>
      <h3>Modes of Interaction</h3>
      <a href="#modes-of-interaction">
        
      </a>
    </div>
    <p>We engage with the IETF in the specification, implementation, experimentation, and analysis phases of a standard to help achieve our three goals of incremental innovation, architectural advancement, and production of simple abstractions.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/0tbHRFLSsWV7qBNiKi4WN/34d3b7742fe21500bcaa4970729bd4e6/image3-20.png" />
            
            </figure><p>Our participation in the standards process hits all four phases. Individuals in Cloudflare bring a diversity of knowledge and domain expertise to each phase, especially in the production of technical specifications. Today, we also published <a href="/exported-authenticators-the-long-road-to-rfc/">a blog post</a> about an upcoming standard that we’ve been working on for a number of years and will be sharing details about how we used formal analysis to make sure that we ruled out as many security issues in the design as possible. We work in close collaboration with people from all around the world as an investment in the future of the Internet. Open standards mean that everyone can take advantage of the latest and greatest in protocol design, whether they use Cloudflare or not.</p><p>Cloudflare’s scale and perspective on the Internet are essential to the standards process. We have experience rapidly implementing, deploying, and experimenting with emerging technologies to gain confidence in their maturity. We also have a proven track record of publishing the results of these experiments to help inform the standards process. Moreover, we open source as much of the code we use for these experiments as possible to enable reproducibility and transparency. Our unique collection of engineering expertise and wide perspective allows us to help build standards that work in a wide variety of use cases. By investing time in developing standards that everyone can benefit from, we can make a clear contribution to building a better Internet.</p><p>One final contribution we make to the IETF is more procedural and based around building consensus in the community. A challenge to any open process is gathering consensus to make forward progress and avoiding deadlock. We help build consensus through the production of running code, leadership on technical documents such as QUIC and ECH, and even logistically by chairing working groups. (Working groups at the IETF are chaired by volunteers, and Cloudflare numbers a few working group chairs amongst its employees, covering a broad spectrum of the IETF (and its related research-oriented group, the <a href="https://irtf.org/">IRTF</a>) from security and privacy to transport and applications.) Collaboration is a cornerstone of the standards process and a hallmark of Cloudflare Research, and we apply it most prominently in the standards process.</p><p>If you too want to help build a better Internet, check out some IETF Working Groups and mailing lists. All you need to start contributing is an Internet connection and an email address, so why not give it a go? And if you want to join us on our mission to help build a better Internet through open and interoperable standards, check out our <a href="https://www.cloudflare.com/careers/jobs/?department=Technology%20Research&amp;location=default">open</a> <a href="https://boards.greenhouse.io/cloudflare/jobs/3271134?gh_jid=3271134">positions</a>, <a href="/visiting-researcher-program/">visiting researcher program</a>, and <a href="https://www.cloudflare.com/careers/jobs/?department=University&amp;location=default">many internship opportunities</a>!</p> ]]></content:encoded>
            <category><![CDATA[Research]]></category>
            <category><![CDATA[IETF]]></category>
            <category><![CDATA[Protocols]]></category>
            <category><![CDATA[Standards]]></category>
            <guid isPermaLink="false">72sMlOH9eqnKfiCmxSGHnU</guid>
            <dc:creator>Jonathan Hoyland</dc:creator>
            <dc:creator>Christopher Wood</dc:creator>
        </item>
    </channel>
</rss>