My latest configuration tweaks (besides adding a little more security here and there) have been related to... HTTP/3. Aye, that's not a typo: HTTP/3. Some of you have been increasingly seeing wider support of the last-before-latest HTTP protocol update, HTTP/2, which has been around for a while, and has been the only update from the ancient HTTP/1.1 (introduced in 1997, reformulated in 2014). You may wonder why so many versions were necessary. Well, up to HTTP/1.0, you had to establish one connection for each resource you wished to download. Because back then web pages were little more than a handful of 'raw' HTML, this would suffice. Establishing connections over port 80 or 443 — using TCP/IP — is a relatively costly (and thus slow) operation, but it would be fine anyway, since the actual download of a single 'pure' HTML page would take way longer than the actual establishment of the connection.
That certainly improved things, but just up to a limit. The 'funneling' through a single pipe certainly saved resources, but... what if the pipe got 'clogged', in the sense that there was either too much data to be transferred and either side was not able to deal with it? Web servers, these days, are mostly threaded (or use similar methods) to allow for 'slower' connections to be still 'kept alive', while continuing to accept requests from other threads. Again, this will work relatively well unless way too many 'slow browsers' are consuming all the resources...
Around half the websites in the world use HTTP/2. On my PPP server, it's the default for all sites, even though almost all of them have the extra protection layer of Cloudflare on top (which provides HTTP/2 anyway). There is a noticeable performance difference when requesting content from the same website using either HTTP/1.1 or HTTP/2. HTTP/2 (always using encryption) will even beat unencrypted HTTP/1.1 connections by a far margin. No wonder that it's quickly becoming the 'standard' in web communications.
Or perhaps not. Around 2018, more researchers at Google started work on the next-generation Web protocol, which they called QUIC (you can see Google's naming trend, right? 😅). The problem, this time, is an order of magnitude greater. You see, TCP/IP is the most universally used transport protocol on the Internet (with the exception of DNS and a few other specialised utilities; see below), because, from a programmers' perspective, it's easy to write applications on top of it. TCP/IP guarantees that a connection is established. It automatically handles things like missing packets, retransmitting them if needed. Both sides of the connection are constantly verifying and validating that the connection is still 'up', and that packets are not coming out of order. Due to the nature of the Internet — its biggest asset! — IP (Internet Protocol) packets can take all sorts of routes from the sender and the receiver, navigating across the interconnected network (aye, that's where the name Internet comes from!), trying to find the 'optimum' path which is not necessarily the one that is geographically shorter — since packets will try to avoid congested routes and search for alternatives when available. TCP, after all, stands for Transmission Control Protocol, and that's exactly what it does.
Here is a short overview on how this works (it's a gross oversimplification). TCP/IP requires that the sender knows the IP address of the destination and a port (for instance, 80 for HTTP or 443 for HTTPS). Ports under 1024 are standardised and can only be used for specific applications (in general, an 'application' is a client-server pair of software programmes which use a well-known port to communicate); above that, anyone can use them freely.
The sender first has to make sure that 'something' is listening to the service before sending anything. This is known in the telecommunications industry as handshaking. So, the sender creates a packet with its own address (so that the receiver knows how to 'talk back') and a port, and pushes it into the Internet, starting a timer. Eventually, this packet will get to the receiver. If that happens, the receiver 'writes back' — i.e. it creates its own packet saying 'ok, I got your request to establish communications, please acknowledge this packet and then you can start communicating'. Let's assume that this packet reaches the sender before the timer reaches zero. It now knows that someone is listening, and that they're clear to send. So first they acknowledge that the communication is 'open' by sending another packet, saying 'ok, I'm acknowledging your packet, communications are now open, please stand by for data'. And then it starts to send packets, labeling them with a series of sequential numbers, so that the receiver manages to figure out the correct order. During the handshaking, they also agree upon a common 'connection number', so that each side can differentiate specific packets belonging to one communication or another. After all, the browser can communicate with several different web servers at the same time; while naturally a web server will need to accept different communications from hundreds, thousands, millions of simultaneous browsers, all of them connecting to ports 80 and/or 443 — so they use a special connection number for each communication (there is obviously a limit to each side, beyond which no further communications can be made simultaneously — one has to wait until some communications are closed to accept more).
Assume that the sender wants to send packets #1, #2, and #3 to the receiver. It starts sending packet #1. If the receiver gets that, it sends an acknowledgement back saying, 'ok, #1 received, proceed to #2'. If that acknowledgement is properly received, the sender then sends packet #2, and so forth. Note that the receiver knows what packet to expect next: if it has just acknowledged packet #2745, it knows that the next one has to be #2746.
Eventually, the sender finishes sending everything, and then there is a different handshake to terminate the connection. The sender will just say something like, 'ok, I have no more packets, this is my last message'; the receiver acknowledges the message by sending back: 'fine with me, last message received, this is the last message you get from me, I'm closing connections now'. If the sender wishes to push a few more packets to receiver, it has to start a new communication. It's as if the receiver has suffered from memory loss and forgot all about what the sender has been sending. The new communication will be treated independently, using a new communication number, and from the outside perspective, it's not even obvious that the same client-server pair is communicating again.
Ok, let's now see how this works in a non-ideal scenario. In reality, things are not so easy, of course. Packets get lost; acknowledgements to packets get lost as well; or they are received out of order. For instance, imagine that things have been going rather well until packet #2745 is received; now the receiver expects #2746. But, instead, let's say that it received #2745 again (there are several reasons for that actually happening). The receiver has now a few choices:
Complicated? Wait until we see the other, more frequent situation: packet loss.
In this scenario, we imagine a 'bad' connection (either a very noisy channel, or a very congested line with not enough bandwidth, or some bad router in-between which will occasionally drop packets, or... well, there are a billion good reasons for communications to fail. Remember that the Internet was designed in 1969 to resist a direct nuclear attack. Things are supposed to continue to work — even if not as well as before! — when that happens). So, the sender creates packets #1, #2, and #3, and starts sending #1. This is received, so the receiver sends back the acknowledgement packet for #1 — let's write it as ACK#1. The sender receives ACK#1, so now both sides now that they can go ahead with further packets. The sender sends #2... but this fails to reach the receiver. So at this point the sender doesn't know if it will ever get an ACK#2, while the receiver is thinking 'ok, I got #1 — but did the sender receive my ACK#1? Or did they already send #2? But I received nothing...'
At this point we're in an indeterminate scenario, since both sides cannot agree any more on the packet sequence. The sender knows that #1 was sent and properly received, but doesn't know if #2 was received or if it never received ACK#2. What should it do? Wait longer (the network might be just slow and the acknowledgment may just be taking more time than usual)? Or send #2 again, and expect that it works this time? What if it doesn't? (let's assume that after trying a thousand times, something must be wrong, so the connection should be closed). Or assume that #2 was received, and, not to waste time, send #3, and expect that ACK#3 is sent back? (and possibly ACK#2 as well?)
On the side of the receiver, there are also several issues. Packet #1 was received... but nothing else since that. So, should the receiver send ACK#1 again — assuming that it was 'lost'? Or ask for #2 to be retransmitted, informing the sender that they didn't receive anything after #1? Or wait longer?
What exactly happens is beyond my knowledge, but I can assume that there are different strategies, and that possibly one can establish a TCP/IP connection with different parameters to coordinate the strategy. For instance, imagine that the sender has no trouble sending packets, but has some difficulty in receiving packets. If this asymmetry is known before the communication is established, the sender may say, 'look, I will be a bit slow when receiving your ACKs, so please wait longer for my next packets' or even 'because I'm slow in receiving your ACKs, please only send one ACK for every ten packets I send'.
The latter idea is actually much closer to how the TCP/IP protocol works in reality. Because TCP packets are small (usually 4096 bytes each, with a bit over 1000 bytes for the ACKs), when transferring a lot of data (such as a large GIF!) it makes more sense to send a lot of packets before waiting for an ACK for all of them. In fact, this is a simple way for each side to figure out the 'best' transfer rate between both: they start sending a certain number of packets, see if they get all properly received, and, if that's the case, they might agree to transmit more packets the next time, and so forth, until an equilibrium is found: sending less packets each time is a waste of the available bandwidth but is 'safer'; sending too many packets (more that the other side can receive!) means missing a lot of ACKs and thus being forced to retransmit everything again (possibly sending less packets the next time). I'm not even going to address these issues of automatic fine-tuning of the 'best' way to send packets — there are dozens of algorithms to deal with traffic congestion, high-bandwidth local networks, low-bandwidth international networks, asymmetric bandwidth (such as what most people have at home), very-high-bandwidth but very high latency (a satellite downlink), and so forth.
And I've not even touched the issue of dealing with communication errors inside packets. Suppose that we send packet #2345, but, when it arrives, instead of the expected 4096 bytes, it has only 3000. What to do next? Ask for a retransmission of the whole packet, or just for the last 1096 bytes? What if those last bytes are also truncated? Wouldn't it make sense to ask for the whole packet instead? And what should be done if the 4096 bytes are received but... during the transmission, there were errors inside it? (some 0s flipped to 1s and vice-versa) How does each side know that there were errors? Can some of these be fixed without asking for a retransmission? (The short answer is 'yes, up to a certain degree of errors'; the long answer requires learning about a gazillion of different methods to allow for error detection — 'checksums' — and error correction; a few of which are available for TCP/IP, others have to be done at a higher layer).
And then we have other considerations to deal with: what about compression and decompression? Most compression algorithms work only well with a large amount of data; thus, compressing individual packets is rarely useful. On the other hand, if one communication is consistently sending, say, 20 packets at the time, wouldn't it be worth to compress them before sending them? How will this affect the sequence numbering? What if some of these packets get lost and/or corrupted? How can each side of the connection figure out how to 'restore' the compressed data?
... and we could also start addressing encryption. To prevent things like packet sniffing, or spoofing, would it make sense to encrypt everything before sending each packet (or group of packets)? Or leave encryption to the 'upper layers' of the software and just send everything in 'clear text' at the TCP level? (the latter is usually what is implemented)
Quoting directly from the Wikipedia:
TCP is complex.
Deliberately so. The idea is that 'the network' does all the tough work of keeping a communication up and flowing packets between sender and receiver, whatever happens 'down below'. That's why typical higher-end communications — such as mail or web access — can be developed with all the above assumptions in place:
Simple, right? There is a bit more to that — especially regarding processing forms or uploading data, figuring out what encryption ciphers should be used, dealing with malicious attacks — but this is HTTP/0.9 in its essence. It's simple and it works — because the layer beneath, TCP/IP, does all the dirty work. At the HTTP level, things are absurdely simple, and that's one of the (many) reasons why HTTP became so popular mere instants after Tim Berners-Lee invented it.
Ok. Although I just showed an oversimplification — nay, an extreme oversimplification — of how TCP/IP works, I hope that you can appreciate that all this complexity has a huge benefit, but it comes at a cost: there is a lot of overhead even for the simplest communications. With 4 or so billion people on the Internet, and at least as many web servers out there, with gazillions of pages/images/videos to be transferred, this means that browsers and web servers all over the world are constantly opening and closing connections, counting packets, setting up timers to wait for a reply, verifying if all packets have been sent correctly and received correctly, dealing with errors, retransmissions, different speeds — both on the telecommunications side of things, but also on the hardware itself: a slow, underpowered server will not be able to handle packet sending and receiving at very fast speeds, even if the network infrastructure is able to do so; on the other hand, the fastest supercomputer in the world will always be limited to the available network bandwidth (and thus wasting all that CPU power endlessly waiting for the network to do its magic...).
And while it's easy to understand why programmers are so fond of the TCP/IP model to develop higher-end applications, one might wonder if there isn't an alternative for those who do not mind getting their hands dirty in exchange of blindingly fast performance.
Well, the second most used protocol on the Internet is User Datagram Protocol, or UDP. Developed in 1980, it is a protocol that doesn't guarantee much — no delivery guarantees and no guarantee that there is even anyone still listening to the packets. There is no connection handshaking, no packet acknowledging, no sequence numbering, no error checking, no timers... nothing. It basically works this way:
After reading about TCP/IP, you might be wondering if UDP/IP isn't absolutely useless!
As said, DNS (Domain Name System) is probably the most known application which uses UDP, because it fits perfectly in this model. Let's assume that A is a computer which wants to know the IP address assigned to a certain server name. B, C and D are domain name servers, running what is called a name resolver, an application which looks up a table of names and their IP addresses.
Here is what DNS works in its simplest form:
10.0.0.1. Have a nice day!
or perhaps more like this:
Ok... so, perhaps it's not exactly like that, but you see how it works:
Other common examples of using UDP are the Network Time Protocol (a way to synchronise all computer clocks in the world — they might not be always right at all the time, but, eventually, each and every computer will get some answer from one time server which is enough to adjust their own clocks) and... highly interactive games. This might sound surprising for such an unreliable protocol, but the truth is that on multiplayer games 'losing' a packet or two (sending positioning data, or information about a shot being fired or the ball being kicked) is not relevant. Games will use predictive algorithms to try to 'guesstimate' where the other players will be, based on packets already received, and draw the scene accordingly; a few packets lost for a few microseconds might not be relevant, and once they are received, the overall scene can be adjusted. Obviously, the more reliable the connection, and the faster the computer used to play, the higher the likelihood of successfully sending and receiving UDP packets. But take into account that, for a game displaying 60fps (enough to create a very convincing illusion of 'smooth' animation), the system has roughly 16 milliseconds to draw each frame. On a superfast local Ethernet connection you can expect ping times of 50 microseconds — plenty of time to exchange quite a lot of data and still have enough CPU cycles to do all those frames! — but even reaching out from my home fibre-installation, I reach the domain server I use from Cloudflare in 2-3 milliseconds, which is really not bad (I'm actually accessing a server installed in my country, thus the short delay). Note that the
ping command uses UDP — if 18.104.22.168 were a game server, then I'd have plenty of milliseconds left to draw a frame.
(Again, take into account that all the above is another oversimplification: in practice, games will not need to get updates every frame; also, it's not likely that each communication packet with just 4096 bytes will be enough for everything — consider the download of a 'new' 3D object and especially all its textures! Obviously, things are not as simple as I have described...)
Okay. This should be enough to make you wonder why there aren't more communication protocols based on UDP, since it seems to be so blindingly fast and tremendously helpful to avoid all the problems and issues that TCP has. The simple answer is that UDP is 'too bare', or 'too raw'. It is very useful in situations where transmitting and receiving all the packets is not crucial. It's good when either the client or the server doesn't really need to 'track' anything (i.e. a DNS server will just reply to whatever request they receive — it's not important to keep track of each individual communication, or if the same packet is sent a million times because the network is down, etc.). But TCP has the advantage of guaranteed delivery — at the cost of having a lot of overhead. As the Internet becomes more and more complex, TCP has become an even more complicated protocol — these days, having to deal with congestion issues and multiple pipelines using the same channel, not to mention things like VPNs, NAT and reverse-NAT (look up all of those nifty features), as well as a hundred (a thousand?) options to configure and setup a specific communication channel — how many packets to send/receive before acknowledging them, for instance; how much memory to allocate to buffers; how much time to wait until an expected reply comes; and so forth.
So let's step up again from the lowest levels of data communications and get back to the application layer, where the developers' worries centre around delivering as much Web content in the least amount of time possible.
Around 2018 or thereabouts, the QUIC protocol, which I mentioned above, emerged from the research labs and started being tested outside the lab environment; shortly afterwards, it was adopted as a brand new Internet standard, HTTP/3, although some changes have been made to QUIC (there were a few security flaws present in the encryption algorithm originally used by Google, so this required some fixing for the 'final' protocol). Google obviously implemented it on their own servers and made Chrome support it (with some caveats...) — if you use a 'modern' browser these days, you'll access everything on Google's servers via HTTP/3. Not to be left behind, Facebook implemented it on their own systems as well. Perhaps the biggest advantage of having a protocol over UDP is the way video streaming can be handled. Video can afford to lose a few packets here and there and still deliver enough quality to the end-user; most importantly, the 'dirty' work of handling things like a slow server, a slow browser running on a slow computer, and a congested network — all that is a problem to be solved at the HTTP/3 level, not at the top levels. If you have an HTTP/3-capable browser, you may have noticed that, these days, videos on YouTube do not 'degrade' their performance substantially during a video session (i.e. offering 720p, downgrading to 480p if the connection is bad, eventually going towards further degradation until the transmission becomes stable). There are many reasons for that, but one of those is that video over HTTP/3 is far less congested and degradation is less noticeable. Why? Well, consider the differences that we have seen between a TCP/IP and a UDP/IP connection. Under TCP/IP, what happens if a user has such a bad connection to the Internet that it cannot receive large video packets at a rate required for a certain resolution (480p, 720p, 1080p, 4K, etc.)? Well, both sides have to negotiate constantly what transmission rate is considered acceptable — if packets start to get dropped too often, the receiving side will inform the server side to 'slow it down', which, in turn, communicates that to the application level: 'here is a user which cannot receive packets so quickly, we have to send them less packets, and wait more time between each'. The application then says, 'well, ok, let's re-establish the connection, but this time, we'll send 480p video as opposed to 720p, and see if they can handle that'. This may happen every few seconds, as communication quality fluctuates, as well as congestion levels increase or decrease. Thus, using HTTP/1.1 or HTTP/2, a lot of work is being done at the transport level (and below) to keep the connection 'live', at the cost of a decrease of performance; and, eventually, connections break and have to be re-established. All this takes up time and resources.
These can be avoided with HTTP/3. UDP packets will be sent and eventually received. If they are, it's up to the receiving end to assemble them together — as best as they can — and as fast as possible. If they are enough to deliver smooth 720p or 1080p video — great. If there is serious congestion and packets get dropped, tough — your browser might drop a few frames now and then, but, overall, the quality will not degrade much, or, if it does, it will recover quickly. I'm obviously not familiar with the details of the implementation, but I can imagine that the UDP packets will just carry some identifier to allow them to be ordered. MPEG-4 (or WebM) video is lossy anyway: if you send out 1000 packets, but #3-#45 are not received, the difference in the way the frames get assembled is not very relevant: you lose some quality on a few frames, but the human eye is not that sensitive to such details. That's a reason why video streaming protocols such as the Real-Time Streaming Protocol, which runs on top of RTP, which, in turn, is implemented using UDP/IP.
Granted, even a stream of video frames requires some form of control; one thing is dropping a few frames now and then, or receiving packets out of order — which will make little difference if they're being stored in a buffer before viewing — but there are other things that also require attention: namely, the user may pause the video, and that means sending back information which requires a degree of reliability (if the pause button doesn't work the first time — due to a lost packet — that wil be fine, but if it happens over and over again, the user gets annoyed!); similarly, users might wish to reset the video size (or its quality), add higher-quality audio (or not at all!), get sync'ed subtitles, and so forth. Clearly, there has to be a way to implement such things using a 'connectionless' protocol such as UDP. For instance, the companion protocol to RTP is RTCP (Real-Time Control Protocol) and to handle things like network congestion there is even RSVP (Resource Reservation Protocol — the acronym is not a coincidence, of course!).
It's legitimate to ask if so many things running on top of good, old, absolutely unreliable UDP won't make it as slow as TCP! The answer is 'no'. TCP does all of that and way, way more. When streaming video (or dealing with positioning data on first-person-shooter games), having a fast, near-real-time communication channel that is 'lossy' is often more than acceptable, when the alternative is 'slow' TCP. There are image/video formats that are already lossy to begin with; with a few tricks, these can benefit from a lossy — but much faster! — transport mechanism.
The same is not true to other things, of course, such as, say... web pages. Sure, web pages also have things like JPEGs — images compressed in a lossy way! — and even video. Also, you can get a lot of things out of order (which, these days, happens anyway under HTTP/1.1 and/or HTTP/2...) and things will still 'work'. If an image is downloading slower than expected, the rest of the webpage can be rendered until the browser finishes the download; the user will see a completely-drawn page with just a missing image. Interlaced JPEG can even be progressively loaded, showing a low-rez image first but which will get rendered with more and more quality as further data is received. These days, images load so fast even in residential broadband that such benefits are often overlooked.
The good news is that, in general, images and video consume far more network resources than plain text (which can even be minified and compressed before sending them to the browser). Thus, a careful balance of what requires being 'intact' at the browser end, and what can eventually be 'partial' or incomplete — while still allowing the page to load and be useful! — might result in better performance overall, less congestion, and the ability for the server to dispatch answers simultaneously via 'connectionless' mechanisms to a multitude of browsers waiting for content, without worrying about the rigours of establishing a full TCP connection and carefully marshalling packets through the 'pipe'. Instead, packets are sent out under a 'fire and forget' policy — some may reach the destination, some may not, and some may end up with errors or out of order; in many cases, clever buffering and a few tricks of the trade will allow the receiving end to make some sense of what has been already received and eventually assemble whatever it can into what looks like the intended web page. This sounds like a very rough, 'patchy' solution just to slice a few milliseconds from a Web connection; but all those milliseconds count, especially for the Google algorithms which give better ranking to websites that answer more quickly.
Therefore — QUIC, or, better, HTTP/3 (which can be understood as 'an implementation of HTTP implemented over QUIC'). Google and Facebook, at least, believe that it's worth the trouble — and so does Cloudflare. Because almost all of my websites in my server use Cloudflare as their frontend, this means that all are HTTP/3-enabled (there are two exceptions — which, for many reasons, cannot be directly served from Cloudflare, so I've attempted to patch
nginx in order to get HTTP/3 support. Sometimes it even works).
To test HTTP/3 with a compliant browser, you can take a look at https://cloudflare-quic.com/ — it will tell you if your browser is HTTP/3-compliant or not. Sometimes, due to the nature of how HTTP/3 works, you will need to refresh the page a few times. Or delete the cache for the page and try again later. Or sometimes it won't work at all. However, I have found that at the very least Google, Facebook and Cloudflare (meaning: everything that is hosted 'behind' Cloudflare and has the switch for HTTP/3 toggled to 'on') are very reliable: there is next-to-zero difference between accessing a page via HTTP/2 or HTTP/3 — with the difference that HTTP/3 pages load much faster (you can do some tests!). Then again, I wonder if this is the case on very congested networks — will HTTP/3 outperform HTTP/2 in such scenarios? If yes, then we can safely predict that HTTP/3 will be 'the future' of the Web. If not, well... it was an interesting attempt, and it may consume far less resources on the server side of things, so it might be still around for a while. Or... they might tweak the protocol (still under development!) to deal better with some edge situations and critical scenarios.
The ever-so-useful Can I Use website also keeps track of which browsers already support HTTP/3. At the time of writing, under Safari 14 (macOS and iOS), this comes out of the box — just point to Facebook, any Google site, any site that has Cloudflare enabled, and that's it. You can use the Inspector from the Developer Tools to verify which sites have been downloaded using HTTP/3. Because the standard is not 'fixed' yet, when using the Developer Tools, under the Network tab, you will have a way to enable a column for Protocol (it's disabled by default), and HTTP/3 connections will be listed as
h3-XX, where the
XX stands for the supported draft specification (at the time of writing,
h3-29 seems to be the most frequent version, although there are already a few more recent drafts).
Currently, besides Safari, both Chrome (and Chromium) and Firefox support HTTP/3. Under Firefox, you just have to go to
about:config and set
network.http.http3.enabled to true (and relaunch). Chrome needs some command-line arguments, but it should work as well. Because Microsoft Edge is based on Chromium, you should be able to do the same command-line trick, but it seems only to work for Microsoft Edge Canary for now. This is due to change quickly, so I imagine that the next time you read this article, every major browser will support it.
Do not despair if things don't work quite as expected. For instance, the latest versions of Firefox somehow don't like the Cloudflare implementation of HTTP/3 — but they have no trouble with Google and/or Facebook. This is normal: the draft specifications are in flux and due to be implemented by browser developers at different paces. And, as said, at the other side of the equation — the web server — things can also be weird to figure out. For instance, at some point, I managed to get my non-Cloudflare'd websites respond perfectly to HTTP/3. Then they started showing a few quirks, now and then — such as pages taking too much time to load, needing a refresh, eventually timing out. And one day I woke up to find that none of my web sites worked. I don't remember having done anything overnight... but, alas, such is the nature of HTTP/3: it's not that easy to fine-tune, and probably I added something (or deleted something!) which made things stop working.
Note that in a well-designed 'fallback' scenario, the browser will try to communicate with the web server establishing a HTTP/1.1 connection, and announce what kinds of protocols it supports (even
https is often negotiated this way, especially if you're visiting a web site for the first time). If both sides can agree on the same draft of HTTP/3 they support, the connection will be 'upgraded' to HTTP/3, and the browser will 'remember' that choice — the next time, it will simply make requests using HTTP/3 and don't even bother opening TCP connections and using plain old HTTP/1.1. If they cannot negotiate a HTTP/3 connection, they will fall back to HTTP/2 — and the browser will also 'remember' that HTTP/3 didn't work but HTTP/2 was fine, so, the next time, it will use HTTP/2 instead.
It seems that this 'remembering' is sometimes counter-productive. For instance, I believe I might have added or deleted something on my server configuration, and all of a sudden, HTTP/3 support was 'broken'. However, because the browsers 'remembered' that I used HTTP/3 before, they tried that again — and failed. So they fell back to HTTP/2 — which worked, so they remembered that instead. From now on, they're 'stuck' with HTTP/2, and even deleting cache, cookies, and so forth, they will stubbornly refuse to attempt a HTTP/3 connection again...
Oh well. I know that it will eventually work exactly as intended. There is still a long way to go... but somehow it feels exciting! 😄