Stream-Level vs Connection-Level Flow Control in XQUIC: Implementation Guide

Stream-level flow control limits the amount of data that can be sent on a single QUIC stream, while connection-level flow control limits the total data across all streams combined.

XQUIC, Alibaba's open-source QUIC protocol implementation, employs a two-tier flow control mechanism that follows the IETF QUIC specification. Understanding the distinction between these limits is essential for debugging performance bottlenecks and optimizing data transmission in multiplexed connections. This guide examines the source code in alibaba/xquic to explain how these limits are enforced, updated, and handled when exceeded.

What is Connection-Level Flow Control?

Connection-level flow control manages the aggregate data flow across the entire QUIC connection. It prevents a peer from flooding the link and exhausting the receiver's buffers globally by capping the total bytes in flight on all streams combined.

The state is tracked in the xqc_conn_flow_ctl_t structure (defined in include/xquic/xqc_typedef.h). Key fields include:

  • fc_max_data_can_send – The maximum bytes the local endpoint may transmit
  • fc_data_sent – Total bytes already sent
  • fc_max_data_can_recv – The limit advertised by the peer
  • fc_data_recved – Total bytes received

When emitting packets, XQUIC validates the connection limit in src/transport/xqc_stream.c before transmission:

/* Lines 351-359 in xqc_stream.c */
if (stream->stream_conn->conn_flow_ctl.fc_data_sent + stream->stream_conn->pkt_out_size >
    stream->stream_conn->conn_flow_ctl.fc_max_data_can_send) {
    /* Exceeds connection window → block */
    xqc_write_data_blocked_to_packet(conn, conn->conn_flow_ctl.fc_max_data_can_send);
}

What is Stream-Level Flow Control?

Stream-level flow control provides granular back-pressure for individual streams. It ensures no single stream monopolizes available bandwidth, maintaining fairness across multiple concurrent transfers within the same connection.

The xqc_stream_flow_ctl_t structure tracks per-stream limits:

  • fc_max_stream_data_can_send – Bytes allowed on this specific stream
  • fc_max_stream_data_can_recv – Peer-advertised limit for this stream
  • fc_stream_recv_window_size – Current receive window allocation

Before writing data to a specific stream, the implementation checks the per-stream constraint:

/* Lines 364-370 in xqc_stream.c */
if (stream->stream_send_offset + stream->stream_conn->pkt_out_size >
    stream->stream_flow_ctl.fc_max_stream_data_can_send) {
    /* Exceeds stream window → block */
    xqc_write_max_stream_data_to_packet(conn, stream->stream_id,
        stream->stream_flow_ctl.fc_max_stream_data_can_send, XQC_PTYPE_SHORT_HEADER);
}

Key Differences Between Stream and Connection Flow Control

While both mechanisms prevent buffer overflow, they operate at different granularities within the XQUIC architecture:

Scope of Limitation

  • Connection-level: Governs total bytes across all bidirectional and unidirectional streams combined
  • Stream-level: Governs bytes sent on a single stream ID only

Transport Frames

  • MAX_DATA frames advertise connection-wide limits
  • MAX_STREAM_DATA frames advertise per-stream limits

Error Handling When limits are exceeded, XQUIC generates distinct errors defined in include/xquic/xqc_errno.h:

  • XQC_ECONN_BLOCKED (line 101): Generated when the connection cannot send more data
  • XQC_ESTREAM_BLOCKED (line 102): Generated when a specific stream cannot send more data

Implementation in XQUIC Source Code

Initializing Flow Control Limits

Connection limits are established during the handshake via xqc_conn_init_flow_ctl in src/transport/xqc_conn.c (lines 598-604):

flow_ctl->fc_max_data_can_send = settings->max_data;   /* Advertised to peer */
flow_ctl->fc_max_data_can_recv = settings->max_data;   /* Peer-advertised limit */

Per-stream limits are configured when opening streams via xqc_stream_open in src/transport/xqc_stream.c (lines 246-274). For bidirectional client streams, the implementation maps the appropriate transport parameters:

if (type == XQC_CLI_BID) {
    stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_bidi_remote;
    stream->stream_flow_ctl.fc_max_stream_data_can_recv = local_settings->max_stream_data_bidi_local;
    stream->stream_flow_ctl.fc_stream_recv_window_size = local_settings->max_stream_data_bidi_local;
}

Processing Window Updates

When receiving MAX_DATA frames, XQUIC updates the connection receive window in src/transport/xqc_frame.c:

/* Lines 600-610 in xqc_frame.c */
if (new_limit > conn->conn_flow_ctl.fc_max_data_can_recv) {
    conn->conn_flow_ctl.fc_max_data_can_recv = new_limit;
    xqc_write_max_data_to_packet(conn, conn->conn_flow_ctl.fc_max_data_can_recv);
}

Similarly, MAX_STREAM_DATA frames trigger updates to specific streams:

/* Lines 608-610 in xqc_frame.c */
if (new_limit > stream->stream_flow_ctl.fc_max_stream_data_can_recv) {
    stream->stream_flow_ctl.fc_max_stream_data_can_recv = new_limit;
    xqc_write_max_stream_data_to_packet(conn, stream->stream_id,
        stream->stream_flow_ctl.fc_max_stream_data_can_recv, XQC_PTYPE_SHORT_HEADER);
}

Summary

  • Connection-level flow control caps total bytes in flight across all streams using xqc_conn_flow_ctl_t and MAX_DATA frames, providing a global safety net
  • Stream-level flow control limits per-stream bandwidth using xqc_stream_flow_ctl_t and MAX_STREAM_DATA frames, ensuring fairness among concurrent streams
  • Both limits are negotiated via transport parameters (max_data, max_stream_data_*) during the QUIC handshake
  • XQUIC enforces these checks in src/transport/xqc_stream.c before packet transmission, queuing DATA_BLOCKED or STREAM_DATA_BLOCKED frames when limits are exceeded
  • Distinct error codes (XQC_ECONN_BLOCKED vs XQC_ESTREAM_BLOCKED) enable precise debugging of flow control violations

Frequently Asked Questions

What happens when the connection flow control limit is reached in XQUIC?

When the connection-level limit is reached, the code in src/transport/xqc_stream.c (lines 351-359) marks the connection as blocked and generates an XQC_ECONN_BLOCKED error. The endpoint sends a DATA_BLOCKED frame to the peer, signaling that additional MAX_DATA frames are required to resume transmission.

How does XQUIC initialize flow control limits for new connections?

XQUIC initializes connection limits in xqc_conn_init_flow_ctl within src/transport/xqc_conn.c using the max_data transport parameter. Per-stream limits are configured during xqc_stream_open in src/transport/xqc_stream.c (lines 246-274) based on stream type (bidirectional vs unidirectional) and endpoint role (client vs server).

Can a single stream consume the entire connection flow control window?

Without stream-level flow control, a single stream could monopolize the entire connection window. XQUIC prevents this by enforcing separate fc_max_stream_data_can_send constraints for each stream, ensuring that no single transfer can starve others of bandwidth.

Which QUIC frames update flow control windows in XQUIC?

MAX_DATA frames update the connection-level receive window (fc_max_data_can_recv), processed in src/transport/xqc_frame.c (lines 600-610). MAX_STREAM_DATA frames update individual stream windows (fc_max_stream_data_can_recv), handled in the same file (lines 608-610). Both frame types are essential for sliding window updates and preventing protocol deadlock.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →