Skip to main content

SpeedLimit

SpeedLimit is a pass-through tunnel that rate-limits payload bytes with a token bucket. It does not rewrite protocol data, add headers, remove headers, or change the destination context. It only decides whether a payload can pass now, must wait, or should be dropped.

The same bucket accounts for both directions that use it. For example, if one bucket is configured for 1 MB/s, upload and download through that bucket share the same 1 MB/s.

Typical placement​

Stream shaping:

TcpListener -> SpeedLimit -> TcpConnector
TcpListener -> TlsServer -> SpeedLimit -> TcpConnector

Packet or datagram limiting:

UdpListener -> SpeedLimit -> UdpConnector
TunDevice -> SpeedLimit -> UdpStatelessSocket

Use pause mode for stream-like traffic where excess data should wait. Use drop mode for packet or datagram traffic where old packets are usually worse than missing packets.

Stream example​

Each connection gets its own 256 KiB/s bucket, and excess stream data is queued until enough tokens refill.

{
"name": "speed-limit",
"type": "SpeedLimit",
"settings": {
"kilo-bytes-per-sec": 256,
"limit-mode": "per-line",
"work-mode": "pause"
},
"next": "next-node"
}

Shared cap example​

All lines passing through this SpeedLimit instance share one 8 MiB/s bucket.

{
"name": "global-speed-limit",
"type": "SpeedLimit",
"settings": {
"mega-bytes-per-sec": 8,
"limit-mode": "all-lines",
"token-recharge-rate": 20,
"work-mode": "pause"
},
"next": "next-node"
}

Packet drop example​

Each worker gets a 500000 bytes/s shared bucket. Payload chunks that do not fit are discarded.

{
"name": "packet-speed-limit",
"type": "SpeedLimit",
"settings": {
"bytes-per-sec": 500000,
"limit-mode": "per-worker",
"work-mode": "drop"
},
"next": "next-node"
}

Required fields​

settings is required and must be a non-empty object.

Exactly one rate field must be configured:

FieldMeaning
bytes-per-secBytes per second.
kilo-bytes-per-secKibibytes per second, multiplied by 1024.
mega-bytes-per-secMebibytes per second, multiplied by 1024 * 1024.

The selected value must be a number greater than 0. If more than one rate field is present, node creation fails.

You must also configure:

FieldAccepted values
limit-modeper-line, per-connection, all-lines, all-connections, per-worker
work-modepause, drop

These string values are case-sensitive in the current parser.

Optional fields​

FieldDefaultDescription
token-recharge-rate10Token refill interval in milliseconds. Must be greater than 0.

token-recharge-rate is not a bandwidth setting. It controls how often tokens are added to the bucket.

For example:

  • bytes-per-sec: 1000
  • token-recharge-rate: 10

The bucket receives about 10 bytes worth of tokens every 10 ms. A smaller interval usually feels smoother. A larger interval can be more bursty because traffic is released in larger time steps.

Limit modes​

ModeBehavior
per-line / per-connectionEvery Waterwall line gets its own bucket. Two simultaneous lines configured for 1 MB/s can together use about 2 MB/s.
all-lines / all-connectionsAll lines through this tunnel instance share one atomic global bucket across workers. A 1 MB/s setting means the whole node shares 1 MB/s.
per-workerEach worker has its own shared bucket. With 4 workers and 1 MB/s, the practical total can reach about 4 MB/s.

Each bucket starts full and has a capacity equal to one second of the configured rate. This allows an initial burst of up to roughly one second of traffic before the limiter settles into the configured average rate.

Work modes​

pause​

pause mode is intended for ordered stream traffic.

When a payload does not fit in the bucket:

  1. The buffer is queued inside SpeedLimit.
  2. The source side is paused with Waterwall Pause callbacks.
  3. A worker timer waits until enough tokens refill.
  4. Queued data is drained in order.
  5. When the queue becomes empty and external pressure has cleared, the source side is resumed.

If a queued buffer is larger than the currently available token grant, SpeedLimit may send a leading slice of that buffer and keep the remainder queued for a later timer tick. This preserves byte order while shaping the average rate.

pause mode can hold buffers in memory. Very low limits, long-lived stalled peers, or high incoming rates can therefore increase memory usage and latency.

drop​

drop mode is intended for packet or datagram traffic.

For each payload chunk:

  • if the bucket has enough tokens for the whole chunk, the chunk is forwarded
  • if the bucket does not have enough tokens, the whole chunk is discarded

drop mode does not split payload chunks and does not queue them for later. This is usually the better behavior for UDP or packet chains because queued old datagrams often become useless.

Direction and backpressure behavior​

Payload limiting and backpressure are symmetrical:

  • upstream payload uses the same selected bucket logic as downstream payload
  • upstream queued data locally pauses the previous side
  • downstream queued data locally pauses the next side
  • external Pause and Resume callbacks are remembered so SpeedLimit does not accidentally resume a side that is still externally paused

This means pause mode composes with normal Waterwall stream backpressure instead of just sleeping inside the payload callback.

Splice support and bounded retention​

Each direction has a separate FIFO bounded by 16 MiB of logical payload, 32 MiB of retained buffer capacity charge, and 1,024 buffers. Empty buffers still count toward the charge and entry limits. Admission refusal closes the affected TCP connection and releases both queues; accepted TCP bytes are never silently dropped.

A consumer Pause stops the corresponding timer drain. The queued data stays owned by this node, without repeated timer polling while paused. Resume rearms the drain, but the source remains paused until both the local queue hold and external pressure have cleared. If creating or rearming the drain timer is refused, both queues are released and the affected connection closes through its owner. Notification callbacks can close the line safely.

The node advertises kNodeFlagSupportsSplice. Whole-buffer forwarding and queued payloads preserve private pipes. In pause mode, partial grants use sbufMoveRangeTo() to move exactly the granted bytes into an independent pipe, with ordinary fallback if pipe allocation or transfer cannot complete. The older remainder stays ahead of nested input. Token accounting includes resident prefixes and pipe bodies. drop mode makes its whole-buffer decision without reading payload bytes.

Splice I/O still requires support throughout the finalized stream chain. Packet chains and unsupported platforms use ordinary buffers.

Packet-line caveat​

Waterwall packet lines are worker-local helper lines that are reused for many packets. In packet chains, per-line does not mean "per real network flow"; it means the bucket attached to that worker's persistent packet line. If you need packet limiting that scales by worker, use per-worker explicitly. If you need one cap across all workers, use all-lines.

For packet or UDP-style chains, prefer work-mode: "drop" unless you specifically want backpressure and delayed packet delivery.

Finish behavior​

Line state is initialized only by upstream Init. Downstream Init is forbidden and terminates the process through abortProgramNow(1). Downstream payload remains supported after upstream initialization.

Do not place this node between Bridge and ReverseClient; those nodes must be directly adjacent when connected in that topology.

On Finish, SpeedLimit destroys its local line state, cancels any drain timers, releases queued buffers, and then forwards the Waterwall Finish in the same direction.

The tunnel does not prepend data, so required_padding_left is 0.

Common mistakes​

  • Do not configure more than one rate field.
  • Do not use pause mode for packet chains unless delayed packets are acceptable.
  • Do not expect per-worker to enforce one global cap; it scales with worker count.
  • Do not expect per-line in packet chains to mean per-client or per-flow.
  • Do not set an extremely large token-recharge-rate unless bursty delivery is acceptable.

Node Metadata​

Source-backed metadata:

PropertyValue
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayerAnything
layer_group_prev_nodekNodeLayerSameAsNext
layer_group_next_nodekNodeLayerSameAsPrev
required_padding_left0 bytes