How to Implement Custom Congestion Control in XQUIC: A Complete Developer's Guide
To implement custom congestion control in XQUIC, define a callback table implementing the xqc_cong_ctrl_callback_t interface, provide a state object size callback, and assign the table to xqc_conn_settings_t.cong_ctrl_callback before creating a connection.
Alibaba's XQUIC library provides a modular transport architecture that allows developers to plug in custom congestion control algorithms via a standardized callback interface. By leveraging the xqc_cong_ctrl_callback_t structure defined in include/xquic/xquic.h, you can inject your own logic for congestion window management, loss detection, and recovery while the transport layer handles packet scheduling and pacing automatically.
Understanding the Congestion Control Interface
XQUIC isolates every congestion control algorithm behind the xqc_cong_ctrl_callback_t interface. When a connection is created, the transport layer examines xqc_conn_settings_t.cong_ctrl_callback to determine which algorithm to use. If no custom callback is provided, the system defaults to Cubic as implemented in src/congestion_control/xqc_cubic.c.
The transport code selects the congestion control implementation during connection initialization in src/transport/xqc_send_ctl.c (lines 156–165):
if (conn->conn_settings.cong_ctrl_callback.xqc_cong_ctl_init_bbr) {
send_ctl->ctl_cong_callback = &conn->conn_settings.cong_ctrl_callback;
} else if (conn->conn_settings.cong_ctrl_callback.xqc_cong_ctl_init) {
send_ctl->ctl_cong_callback = &conn->conn_settings.cong_ctrl_callback;
} else {
send_ctl->ctl_cong_callback = &xqc_cubic_cb; /* default fallback */
}
The framework requires nine specific callbacks covering initialization, loss handling, acknowledgment processing, and window queries. The complete interface definition resides in include/xquic/xquic.h (lines 1019–1038).
Step-by-Step Implementation Guide
1. Define Your Algorithm State
Create a header file defining your per-connection state structure. This object stores variables like congestion window, slow-start threshold, and recovery timestamps.
/* my_cc.h */
#ifndef MY_CC_H
#define MY_CC_H
#include <xquic/xquic.h>
typedef struct {
uint64_t cwnd; /* Current congestion window in bytes */
uint64_t ssthresh; /* Slow-start threshold */
xqc_usec_t recovery_ts; /* Start of current recovery epoch */
xqc_send_ctl_t *ctl_ctx; /* Reference to send control context */
} my_cc_t;
#endif
2. Implement the Required Callbacks
Create a source file implementing all mandatory functions. The framework invokes these during specific transport events: packet loss, acknowledgments, and congestion window queries.
/* my_cc.c */
#include "my_cc.h"
#include "src/transport/xqc_send_ctl.h"
#include <xquic/xquic.h>
static size_t
my_cc_size(void)
{
return sizeof(my_cc_t);
}
static void
my_cc_init(void *cong_ctl, xqc_send_ctl_t *ctl_ctx, xqc_cc_params_t cc_params)
{
my_cc_t *cc = (my_cc_t *)cong_ctl;
cc->cwnd = XQC_kInitialWindow; /* Default initial window */
cc->ssthresh = UINT64_MAX;
cc->recovery_ts = 0;
cc->ctl_ctx = ctl_ctx;
}
static void
my_cc_on_lost(void *cong_ctl, xqc_usec_t lost_sent_time)
{
my_cc_t *cc = (my_cc_t *)cong_ctl;
if (lost_sent_time > cc->recovery_ts) {
cc->recovery_ts = xqc_monotonic_timestamp();
cc->cwnd = (cc->cwnd * 5) / 10; /* Multiplicative decrease (0.5) */
cc->ssthresh = cc->cwnd;
}
}
static void
my_cc_on_ack(void *cong_ctl, xqc_packet_out_t *po, xqc_usec_t now)
{
my_cc_t *cc = (my_cc_t *)cong_ctl;
if (po->po_sent_time <= cc->recovery_ts) {
return; /* Ignore ACKs from recovery epoch */
}
if (cc->cwnd < cc->ssthresh) {
cc->cwnd += po->po_used_size; /* Slow start: additive growth */
} else {
cc->cwnd += (XQC_kMaxDatagramSize * po->po_used_size) / cc->cwnd; /* Congestion avoidance */
}
}
static uint64_t
my_cc_get_cwnd(void *cong_ctl)
{
return ((my_cc_t *)cong_ctl)->cwnd;
}
static void
my_cc_reset_cwnd(void *cong_ctl)
{
my_cc_t *cc = (my_cc_t *)cong_ctl;
cc->cwnd = XQC_kInitialWindow;
cc->recovery_ts = 0;
}
static int
my_cc_in_slow_start(void *cong_ctl)
{
my_cc_t *cc = (my_cc_t *)cong_ctl;
return cc->cwnd < cc->ssthresh;
}
static void
my_cc_restart_from_idle(void *cong_ctl, uint64_t arg)
{
(void)cong_ctl;
(void)arg;
}
static int
my_cc_in_recovery(void *cong_ctl)
{
my_cc_t *cc = (my_cc_t *)cong_ctl;
return cc->recovery_ts != 0;
}
3. Assemble the Callback Table
Export a global constant containing function pointers for all implemented callbacks. This table serves as the entry point for the transport layer.
/* my_cc.c (continued) */
const xqc_cong_ctrl_callback_t my_cc_cb = {
.xqc_cong_ctl_size = my_cc_size,
.xqc_cong_ctl_init = my_cc_init,
.xqc_cong_ctl_on_lost = my_cc_on_lost,
.xqc_cong_ctl_on_ack = my_cc_on_ack,
.xqc_cong_ctl_get_cwnd = my_cc_get_cwnd,
.xqc_cong_ctl_reset_cwnd = my_cc_reset_cwnd,
.xqc_cong_ctl_in_slow_start = my_cc_in_slow_start,
.xqc_cong_ctl_restart_from_idle = my_cc_restart_from_idle,
.xqc_cong_ctl_in_recovery = my_cc_in_recovery,
};
4. Register with Connection Settings
Assign your callback table to the connection settings before invoking xqc_engine_connect. The transport layer automatically allocates state memory using your xqc_cong_ctl_size callback and initializes it via xqc_cong_ctl_init.
#include "my_cc.h"
xqc_conn_settings_t settings = xqc_conn_get_conn_settings_template(XQC_CONN_SETTINGS_DEFAULT);
/* Install custom congestion control */
settings.cong_ctrl_callback = my_cc_cb;
/* Optional: tune algorithm parameters */
settings.cc_params.max_cwnd = 2 * 1024 * 1024; /* 2 MiB limit */
xqc_engine_t *engine = xqc_engine_create(&engine_settings, NULL);
xqc_connection_t *conn = xqc_engine_connect(engine, &local_addr, &peer_addr,
&settings, NULL, XQC_CONN_TYPE_CLIENT);
Reference Implementations and Key Files
When developing custom algorithms, examine these canonical implementations in the XQUIC repository:
src/congestion_control/xqc_new_reno.c(lines 29–40): Demonstrates a minimal compliant implementation with standard Reno logicsrc/congestion_control/xqc_cubic.c(line 251): Shows the default Cubic algorithm and window calculation patternssrc/congestion_control/xqc_bbr.c(line 1223): Illustrates advanced pacing integration and bandwidth probing
The interface contract is defined in include/xquic/xquic.h (lines 1019–1038), while the callback dispatch logic lives in src/transport/xqc_send_ctl.c (lines 156–165).
Minimal Testing Example
For rapid prototyping, implement a fixed-window algorithm that maintains a constant congestion window regardless of network conditions:
static uint64_t bigwin_get_cwnd(void *c) { return 10 * 1024 * 1024; } /* 10 MiB */
const xqc_cong_ctrl_callback_t bigwin_cb = {
.xqc_cong_ctl_size = bigwin_size,
.xqc_cong_ctl_init = bigwin_init,
.xqc_cong_ctl_get_cwnd = bigwin_get_cwnd,
/* ... remaining no-op callbacks ... */
};
Summary
- XQUIC congestion control is implemented via the
xqc_cong_ctrl_callback_tinterface requiring nine mandatory function pointers - State management relies on your
xqc_cong_ctl_sizecallback to allocate memory, followed byxqc_cong_ctl_initfor initialization - Registration occurs by assigning your callback table to
xqc_conn_settings_t.cong_ctrl_callbackbefore connection creation - Transport integration happens automatically in
xqc_send_ctl.c, which invokes your callbacks during loss detection, ACK processing, and pacing decisions - Reference patterns are available in NewReno (
xqc_new_reno.c), Cubic (xqc_cubic.c), and BBR (xqc_bbr.c) implementations
Frequently Asked Questions
What callbacks are mandatory for XQUIC congestion control?
You must implement all nine fields of the xqc_cong_ctrl_callback_t structure: xqc_cong_ctl_size, xqc_cong_ctl_init, xqc_cong_ctl_on_lost, xqc_cong_ctl_on_ack, xqc_cong_ctl_get_cwnd, xqc_cong_ctl_reset_cwnd, xqc_cong_ctl_in_slow_start, xqc_cong_ctl_restart_from_idle, and xqc_cong_ctl_in_recovery. The transport layer invokes these during specific connection lifecycle events, and missing implementations will cause undefined behavior or compilation failures.
How does XQUIC allocate memory for custom congestion control state?
The framework calls your xqc_cong_ctl_size callback to determine the allocation size, then reserves that amount of memory per connection. During connection initialization, it passes this memory block to your xqc_cong_ctl_init function along with the send control context and congestion parameters. You cast this void pointer to your state structure type and initialize algorithm-specific variables.
Can I change congestion control algorithms after a connection is established?
No. The XQUIC architecture selects the congestion control implementation during connection creation in xqc_send_ctl.c (lines 156–165) based on the cong_ctrl_callback field in the connection settings. Once assigned, the callback table remains fixed for the connection's lifetime. To use different algorithms, you must specify them when creating new connections.
Where can I find working examples beyond NewReno?
Examine src/congestion_control/xqc_cubic.c for the default Cubic implementation including Hystart++ support, and src/congestion_control/xqc_bbr.c for a bandwidth-probing algorithm with pacing integration. Both files demonstrate production-ready patterns for window management, loss recovery, and RTT sampling that you can adapt for custom algorithms.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →