Support for status code 103 "Early hints"

Wanted to get this on the Cloudflare team’s radar. This would be an incredibly powerful feature should it gain traction in user agents.

https://www.fastly.com/blog/beyond-server-push-experimenting-with-the-103-early-hints-status-code

Chrome and Fastly are currently running an experiment to validate a web performance tool — and we need your help!

Remember all of the hype around Server Push? Since HTTP/2 shipped, it’s become increasingly clear that while the performance problem that it was designed to address is important, Server Push was not the right approach. Instead, the Preload mechanism has become a widely-used replacement that delivers most of the benefits of Server Push without the risk or complexity.

However, there are still situations where Preload doesn’t deliver great performance. Andrew Betts, our principal developer advocate, talked about a potential improvement in a blog entry two years ago: the 103 Early Hints HTTP status code, proposed by Fastlyan Kazuho Oku. Unfortunately, that proposal stalled because no browsers implemented Early Hints, so they weren’t useful for most web traffic.

Since then, it’s become clear that preload hints are here to stay; the HTTP Archive’s 2019 Almanac reported that they’re used on 16% of web sites. At the same time, take-up of HTTP/2 Server Push is still around the 0.04% of connections that Chrome reported back in 2017, according to Firefox telemetry.

That makes 103 Early Hints a lot more interesting as a potential performance improvement. However, supporting it is a lot of work for browser vendors, so they want to gather data on how much it improves things before spending the time and engineering resources.

That’s where the experiment comes in.

3 Likes

It seems something has been moving… :wink:

4 Likes

Can you fill us in on what you’re referring to? We’re not all insiders here.

1 Like

I guess they are referring to this new beta: Early Hints: How Cloudflare Can Improve Website Load Times by 30%

3 Likes

Looks like my prayers are being answered. #1, #2.
Finally, things are starting to get better, as in the close future they finally replace Http2/Push with something coequal.

A little disappointed with the blog post, as it does not clearly state how to implement it. It actually just shows what the headers look as an output.

Examples like:

Apache:

(.htaccess)

<FilesMatch "index.html">
    Header add Link "</assets/app.min.css>; rel=preload; as=style"
</FilesMatch>

Nginx:

(nginx.conf)

location ~ /index.html {
    add_header Link "</assets/app.min.css>; rel=preload; as=style"
}

Or something like it, so developers know how to properly implement it to make it work with Cloudflare. But ATM, we just know Cloudflare supports it, and you need Chrome v94 to make it work. But we don’t know how to make our servers actually send the right headers, or if there is anything we must consider.

Examples would be awesome, thanks Cloudflare :slight_smile:

4 Likes

Thanks, I saw it on the blog yesterday too! (Wasn’t there yet when I asked here.)

Good news! :slightly_smiling_face:

1 Like

Hi @M4rt1n,

(Disclaimer: I’m a Cloudflare engineer but have not worked directly on most of Early Hints).

I was doing some performance tuning of a personal website over the holidays and decided to tack on some Early Hints experimentation at the end too. I stumbled across this post and the example you provided was useful in quickly generating a Link header from my origin. So thanks for sharing it!

Unfortunately, while verifying that the 103 response was generated from this Link header I found a bug that can occur and prevent the 103 getting sent. I’ve tracked this down to handling of the type parameter in the edge and have identified a fix. However, that fix will take a bit of time to ship. In the meantime, if anyone is seeing issues with early hints when using these examples I would suggest temporarily avoiding the use of the type parameter. The other parameters rel and as are fine to use without problem.

3 Likes

You are welcome.

Thanks for sharing the bug with the type attribute. The actual reason I actually posted this was because Cloudflare wrote so many blog posts about how nice it is that it now supports Early Hints, but nowhere was written how to implement it. No word mentioned on what header must be set in what way. Also, there till today is no tool to verify the functionality of Early Hints. At least, I am not aware of any, even outside of Cloudflare.

If you could forward that to anyone working on Early Hints at Cloudflare, that would be awesome!

BTW: Early Hints (just like HTTP/2 Push) is having some other problems. Don’t know how to describe that problem, but they in my opinion are limiting competition. Don’t worry, Cloudflare offering it will not get it in any trouble, it’s just that the implementation of Early Hints is blocking growth of new technology like AVIF, JXL etc in favor of WebP. But we can discuss that somewhere else, then here.

1 Like

I have to revert that and want to add some info from HERE. A very easy was to test it is:

curl -X HEAD -I https://your.website.de

Replace “https://your.website.de” with your actuall FULL URL and see the header response. If Early Hints are working it should output something like this:

HTTP/2 103
link: </assets/poppins-400-normal.woff2>; as=font; crossorigin; rel=preload, </assets/montserrat-800-normal.woff2>; as=font; crossorigin; rel=preload, </assets/app.min.css>; as=style; rel=preload, </assets/patrickheldmayer_lossy.webp>; as=image; rel=preload

HTTP/2 200

Thanks @LucasCF I think the type bug kicked in on me earlier thats why I coundn’t verify its working.

1 Like

No problem! I’d over thought your previous comment and inferred you were taking about difficult to prove page load performance gains (which can be true because of all the variables at play). When we are talking about more straightforward “does it work tests” then curl is realy good as long as it uses HTTP/2 or HTTP/3.

Alternatively, you can also try the nghttp2 client tool nghttp (nghttp(1) — nghttp2 1.52.0-DEV documentation). This gives a bit more low-level information about what it happening on the wire. In this example, I purposefully bust the cache to emulate the “server think” time where early hints shines. You can see that the 103 is returned at time 0.091 and then then 200 is returned at 0.333. In my local adhoc tests from residential ISP, I see a consistent delta in the range of 100-250 ms.

$ nghttp https://lucaspardue.com/?cache=bust -nv -H "user-agent: curl/7.68.0"
[  0.045] Connected
The negotiated protocol: h2
[  0.068] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.068] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.068] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.068] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.068] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.068] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.068] send HEADERS frame <length=47, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /?cache=bust
          :scheme: https
          :authority: lucaspardue.com
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: curl/7.68.0
[  0.085] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>
          (niv=3)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):256]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65536]
          [SETTINGS_MAX_FRAME_SIZE(0x05):16777215]
[  0.086] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=2147418112)
[  0.086] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.086] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.091] recv (stream_id=13) :status: 103
[  0.091] recv (stream_id=13) link: </wp-content/themes/twentynineteen/style.slim.css>; as=style; rel=preload
[  0.091] recv HEADERS frame <length=60, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.333] recv (stream_id=13) :status: 200
[  0.333] recv (stream_id=13) date: Fri, 07 Jan 2022 02:11:55 GMT
[  0.333] recv (stream_id=13) content-type: text/html; charset=UTF-8
[  0.333] recv (stream_id=13) link: </wp-content/themes/twentynineteen/style.slim.css>; rel=preload; as=style
[  0.333] recv (stream_id=13) vary: Accept-Encoding
[  0.333] recv (stream_id=13) cf-cache-status: DYNAMIC
[  0.333] recv (stream_id=13) expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
[  0.333] recv (stream_id=13) strict-transport-security: max-age=15552000; preload
[  0.333] recv (stream_id=13) server: cloudflare
[  0.333] recv (stream_id=13) cf-ray: 6c99a87d99fe7480-LHR
[  0.333] recv (stream_id=13) content-encoding: gzip
[  0.333] recv (stream_id=13) alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400
[  0.333] recv HEADERS frame <length=344, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
[  0.334] recv DATA frame <length=7460, flags=0x00, stream_id=13>
[  0.334] recv DATA frame <length=8192, flags=0x00, stream_id=13>
[  0.335] recv DATA frame <length=5888, flags=0x00, stream_id=13>
[  0.335] recv DATA frame <length=0, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.335] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

2 Likes

@LucasCF Thank you I appreciate that hint with nghttp. Will try it.

Yes i wanted a very simple “does it work test”. I want to build a little vanillaJS which terminates if it works on your browser or not. But this does have low priority. Since even if it works, there is no proof that you profit from, since it could be your browser who’s not supporting it. Yes my cURL version supports HTTP/2 so thats fine.

Another question. Did you encounter these problems aswell:

How to reproduce:

  1. Cache a Page with Cache Everything
  2. request untill cached
  3. now automatically detect and count hits and miss of “Early Hints” when calling that page.

My results are shocking.

If tested against Cloudflare (with Cache Everything) the successratio was about 70%.
If tested against Cloudflare (with a dynamic URL) the successratio was about 98%.
When tested against Fastly it was 100%. No miss at all.

Did you encounter any of these behaviours? If you want to test feel free to test against these URLs:

This is explained a bit on Early Hints · Cloudflare Cache (CDN) docs

Early Hints may be emitted less frequently on requests where the content is cacheable. Cloudflare CDN is more likely to retrieve a response header before the asynchronous Early Hints lookup finishes if the response has been cached. Cloudflare will not send a 103 response if the main response header is already available.

In other words, if we can retrieve the actual response quickly from our caches, we prefer to send that over a 103 that would be immediately followed by the 200.

1 Like

Thanks. I understand. On the other hand, I would really love to do some own tests here.
Do you think there is any chance that someone could disable that functionality for me on one zone for a while?

Would that also mean, that how often a 103 is triggered on a well cached page could give some insights of Cloudflare’s current (at least that POP) internal performance?

1 Like