The need for flow control arises in any system, whenever an entity produces items and another entity consumes them. That is, there should be a balance between the rate of production and the consumption to ensure that the sender can’t overwhelm the receiver. This is important in networking protocols as well.
Need for flow control in HTTP/2
In HTTP/2, each request/response message that is exchanged between a client and a server is associated with a stream and a single HTTP/2 connection is capable of handling multiple concurrent streams. Therefore, to ensure that streams in a single connection won’t interfere with each other, the flow control is supported, both at the connection and at the stream level.
HTTP/2 provides flow control via WINDOW_UPDATE frame and remember only DATA frames are subject to flow control. With the WINDOW_UPDATE frame, both the client and the server can independently advertise the number of octets that they are ready to receive and then the peer must respect this imposed value. That is the basic theory behind HTTP/2 flow control.
Flow Control Window
Flow control window is just nothing more than an integer value indicating the buffering capacity of the receiver. Each sender maintains a separate flow control window for each stream and for the overall connection.
WINDOW_UPDATE frame indicates the number of octets the sender can transmit in addition to the existing flow control window. The legal range is 1 to 2³¹ -1 octets.
Initial Flow Control Window Size
According to the HTTP/2 specification, the initial value for the flow control window is 65,535 octets for both the new streams and the overall connection.
However, during the HTTP/2 connection establishment phase, both endpoints can adjust this window size by changing the value of SETTINGS_INITIAL_WINDOW_SIZE property in the SETTINGS frame. But keep in mind that this only applies to streams and not to the connection window. Connection flow control window can only be changed using a WINDOW_UPDATE frame.
When this SETTINGS_INITIAL_WINDOW_SIZE value changes, the receiver must adjust the flow control windows by the difference between the new value and the old value.
Maintaining Flow Control Window Size
Since the flow control is directional, both endpoints maintain stream level and connection level windows. The basic mechanism to maintaining the flow control windows, defined by the spec, is as follows.
- When the sender sends a flow controlled frame(DATA frame), it reduces the space in both windows(stream and connection window) by the length of the transmitted frame.
- When the receiver consumes data, it can send WINDOW_UPDATE frames to the sender to free up flow control windows. The receiver should send separate WINDOW_UPDATE frames for the stream and the connection.
- When the sender receives the WINDOW_UPDATE frames it should update the corresponding windows by the specified value in the frame.
An example of flow control is depicted in the following figure.
However, please note that implementors are free to choose any algorithm they see fit, to decide when to send the WINDOW_UPDATE frames.
Now let’s see an example of HTTP/2 flow control in action.
HTTP/2 Flow Control in Action
I’m going to use ballerina language to build a simple HTTP service(we can get it up and running with a few lines of code) and it will be configured to use HTTP/2 as its protocol. I’ll use nghttp as my test HTTP/2 client.
Following is the simple HTTP echo service that retrieves the incoming payload from the request and sends it back to the caller.
We can start this service by issuing the following command.
ballerina run http2-flow-control-test.bal
Once it is started, you will see a log similar to the following.
[ballerina/http] started HTTP/WSS endpoint 0.0.0.0:9090
We are all set now. So let’s use nghttp to send a request to this service. I’m going to send an 82.6kB size of a JSON file via this request to the backend service. Here’s the command.
nghttp -v -d payload.json --cert myapp.pem https://localhost:9090/echo
Now let’s observe the nghttp log and analyze the HTTP/2 flow. Here, I’m only going to show you the relevant parts of the logs for clarity.
First, the client sends the SETTINGS frame with the initial window size value as 65535 octets. This tells the server, the number of octets, the client is ready to receive and it is followed by a few priority frames (we are not gonna talk about that right now. That’s a whole other topic) after which the client immediately starts to send headers and data frames to the server.
According to the log, we can see that the client has sent four data frames ( (16384 * 3 + 16383 = 65535 octets) to the server. After that client is not allowed to send any more data since the window is now full(flow control window is now equal to the number of octets the client has sent).
Then the client receives a SETTINGS frame from the server and the client immediately acknowledges that. Since that does not contain an initial window value, original 65535 will be used as the default. The client also receives the settings ACK from the server which tells the client that the server has received the settings frame, sent by the client(Figure 2).
So far the client has sent 65535 octets to the server and as I have mentioned before, it can’t send any more data until it receives a WINDOW_UPDATE frame from the server, freeing up its window.
So next the client receives two window update frames each with a size of 65535. Stream id 0 is for the connection window and the stream id 13, in this case, is for the relevant stream. Then the client sends the rest of the data to the server (16384+699 = 17083) in two DATA frames. Altogether now the client has sent 82618 octets which constitutes the full payload.
Next, the client starts to receive a response from the server.
Figure 5 & 6
First, it receives four DATA frames(8192 * 4 =32768 octets).
Then the client sends a window update frame to the server, telling it that the client has consumed 32768 octets and to free up the window space maintained by the server.
Figure 7 & 8
Then the client receives another 4 DATA frames, out of which 3 frames with the size 8192 and another DATA frame with the size 8191. (8192*3+8191=32767).
Then the client again sends back a window update with 32767 octets as its value to the server to make room in the server window so that the server is allowed to send more data to the client.
Figure 9 & 10
Finally, the client receives another two DATA frames with sizes 16384 and 699 respectively which makes up the whole response payload.
Then as the final step, we can see that the client has sent a GOAWAY frame to the server, to close the connection.
This is basically how the flow control works and you can try this out with different initial window sizes by using nghttp ’s -w flag.
Following are some of the error conditions that we have to taken into consideration when implementing flow control.
- WINDOW_UPDATE frame with a value 0 results in a stream error of type protocol error.
- Errors on the connection flow control window are treated as connection errors.
- Flow control window of the sender cannot exceed 2³¹-1 octets. If a sender receives a WINDOW_UPDATE frame that causes the flow control window to exceed this value then it should be treated as an error.
Other Subtle Facts
If you want to know more about the error handling and other subtle facts related to flow control, I suggest you read flow control and window update sections in HTTP/2 specification for a detailed explanation. That being said, this topic, most of the time does not directly concern the users or developers unless you want to develop your own HTTP/2 server or client. But knowledge about this might help you troubleshoot issues when working with HTTP/2.
RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2)
Internet Engineering Task Force (IETF) M. Belshe Request for Comments: 7540 BitGo Category: Standards Track R. Peon…