Core Question
Core Question
QUIC enforces flow control at two levels at once — per stream and per connection. What decides whether a send goes through, and what exactly happens to each level's balance when the answer is no?
Outcome
Outcome
By the end of this session, the learner should be able to:
- state the one field pair (
limit,consumed) that backs both stream-level and connection-level accounting - trace
send_on_stream()'s check order and say which level is tested first - explain why a blocked send leaves both accounts completely unchanged
- explain why
grant()can silently do nothing
Read Order
Read Order
- Read the module comment above
SendDecision(the RFC 9000 §4 reference) - Read
CreditAccount - Read
can_send() - Read
consume() - Read
grant() - Read
send_on_stream() - Run
examples/http-quic/session_09_walkthrough.py
Read It Like Code
Read It Like Code
CreditAccount(
limit, # the most this account may ever have consumed
consumed, # how much has actually been sent against it so far
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
limit | The receiver-granted ceiling. Only grant() can move it, and only upward. |
consumed | Running total of bytes sent against this account. Only consume() increments it, and only after both checks in send_on_stream() pass. |
Decision Flow
Decision Flow
send_on_stream(conn_account, stream_account, n):
1. can_send(stream_account, n) is False -> BLOCKED_BY_STREAM (nothing consumed anywhere)
2. can_send(conn_account, n) is False -> BLOCKED_BY_CONNECTION (nothing consumed anywhere)
3. otherwise -> consume(stream_account, n)
consume(conn_account, n)
SENTReading Lens
Reading Lens
The important move in this session is to stop thinking of "stream limit" and "connection limit" as two different mechanisms and start seeing them as the same CreditAccount shape, checked twice in a fixed order. Read every send_on_stream() call asking:
- which account does
can_send()reject first — is it ever possible to reach the connection-level check before the stream-level one has passed? - on a blocked send, did
consume()run on either account? How would you prove that from the source alone, not just from behavior? - after a
grant()call, is the newlimitalways the value passed in, or only sometimes?
Toy Model Boundary
Toy Model Boundary
Real QUIC flow control is bidirectional and signaled on the wire: a receiver sends MAX_DATA and MAX_STREAM_DATA frames to grant credit, and a blocked sender emits DATA_BLOCKED / STREAM_DATA_BLOCKED frames to say so. None of that framing exists here — grant() is called directly as a plain function, and a blocked send_on_stream() call simply returns an enum member with no signal sent anywhere. There is also no MAX_STREAMS accounting (the limit on how many streams may be open at all) — this module only tracks bytes on streams and the connection that already exist.
Code Landmarks
Code Landmarks
The module comment above SendDecision
Names RFC 9000 Section 4 directly and states the whole model in one sentence: a receiver grants a limit, a sender consumes against it, and the limit only ever moves up. Read this before anything else in the file.
CreditAccount's docstring
"One shape, two levels: the same credit balance backs both a stream and the connection." There is no separate StreamCreditAccount or ConnectionCreditAccount type — send_on_stream() is simply called with two different CreditAccount instances.
send_on_stream()
if not can_send(stream_account, n):
return SendDecision.BLOCKED_BY_STREAM
if not can_send(conn_account, n):
return SendDecision.BLOCKED_BY_CONNECTION
consume(stream_account, n)
consume(conn_account, n)
return SendDecision.SENTThe stream check runs first. consume() is called only after *both* checks pass — there is no code path where one account is consumed and the other is not.
grant()
if new_limit > account.limit:
account.limit = new_limitA single comparison. Calling grant() with a value at or below the current limit is a legal, silent no-op — nothing raises, nothing is logged, limit simply stays put.
Failure Questions
Failure Questions
Use the source file to answer these:
send_on_stream()callscan_send()onstream_accountbeforeconn_account. If a send would be blocked by *both* accounts, whichSendDecisionvalue is returned — does the caller ever learn about the connection-level shortfall?- A call to
send_on_stream()returnsBLOCKED_BY_STREAM. What isconn_account.consumedimmediately afterward, compared to immediately before the call? Which lines insend_on_stream()guarantee this? grant(account, new_limit)is called with anew_limitequal toaccount.limitexactly (not lower, not higher). Doesaccount.limitchange? Which comparison operator ingrant()decides this edge case?can_send()checksaccount.consumed + n <= account.limit. Ifnis 0, cancan_send()ever returnFalse, regardless of how exhausted the account is?- After a
BLOCKED_BY_CONNECTIONresult, could a caller retry the exact samesend_on_stream()call with the exact same arguments and getSENTwithout any interveninggrant()call? What would have to be true aboutstream_accountandconn_accountfor that to happen?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/http-quic/session_09_walkthrough.py
The walkthrough sends within both limits and checks both balances rise together, then exhausts stream-level credit and confirms both accounts are completely unchanged by the blocked send, then shows ample stream credit blocked by a tight connection account, then grants a higher connection limit and watches the same send go through, then shows a lower grant is silently ignored.
Done When
Done When
The learner can say all of the following without looking at notes:
- "CreditAccount is one shape reused at both the stream and connection level — send_on_stream() is what makes it two-level."
- "A blocked send, whether by stream or connection, consumes nothing on either account — consume() only runs after both checks pass."
- "grant() only ever raises a limit; a lower value is a silent no-op, not an error."
References
References
- RFC 9000 Section 4 (Flow Control)
Continue