[benzedrine.ch logo]
Daniel Hartmeier
Packet Filter
Mailing list
Annoying spammers
Prioritizing ACKs
Transparent squid
Planet Wars
Hexiom solver
Polygon partition
Mikero's grid puzzle
Dark Star

Prioritizing empty TCP ACKs with pf and ALTQ


(there's a french translation of this page by Alexandre Anriot)

ALTQ is a framework to manage queueing disciplines on network interfaces. It manipulates output queues to enforce bandwidth limits and prioritize traffic based on classification.

While ALTQ was part of OpenBSD and has been enabled by default since several releases, the next release will merge the ALTQ and pf configuration into a single file and let pf assign packets to queues. This both simplifies the configuration and greatly reduces the cost of queue assignment.

This article presents a simple yet effective example of what the pf/ALTQ combination can be used for. It's meant to illustrate the new configuration syntax and queue assignment. The code used in this example is already available in the -current OpenBSD source branch.


I'm using an asymmetric DSL with 512 kbps downstream and 128 kbps upstream capacity (minus PPPoE overhead). When I download, I get transfer rates of about 50 kB/s. But as soon as I start a concurrent upload, the download rate drops significantly, to about 7 kB/s.


Even when a TCP connection is used to send data only in one direction (like when downloading a file through ftp), TCP acknowledgements (ACKs) must be sent in the opposite direction, or the peer will assume that its packets got lost and retransmit them. To keep the peer sending data at the maximum rate, it's important to promptly send the ACKs back.

When the uplink is saturated by other connections (like a concurrent upload), all outgoing packets get delayed equally by default. Hence, a concurrent upload saturating the uplink causes the outgoing ACKs for the download to get delayed, which causes the drop in the download throughput.


The outgoing ACKs related to the download are small, as they don't contain any data payload. Even a fast download saturating the 512 kbps downstream does not require more than a fraction of upstream bandwidth for the related outgoing ACKS.

Hence, the idea is to prioritize TCP ACKs that have no payload. The following pf.conf fragment illustrates how to set up the queue definitions and assign packets to the defined queues:


altq on $ext_if priq bandwidth 100Kb queue { q_pri, q_def }
queue q_pri priority 7
queue q_def priority 1 priq(default)

pass out on $ext_if proto tcp from $ext_if to any flags S/SA \
        keep state queue (q_def, q_pri)

pass in  on $ext_if proto tcp from any to $ext_if flags S/SA \
        keep state queue (q_def, q_pri)
First, a macro is defined for the external interface. This makes it easier to adjust the ruleset when the interface changes.

Next, altq is enabled on the interface using the priq scheduler, and the upstream bandwidth is specified. I'm using 100 kbps instead of 128 kbps as this is the real maximum I can reach (due to PPPoE encapsulation overhead). Some experimentation might be needed to find the best value. If it's set too high, the priority queue is not effective, and if it's set too low, the available bandwidth is not fully used. Then, two queues are defined with (arbitrary) names q_pri and q_def. The queue with the lower priority is made the default.

Finally, the rules passing the relevant connections (statefully) are extended to specify what queues to assign the matching packets to. The first queue specified in the parentheses is used for all packets by default, while the second (and optional) queue is used for packets with ToS (type of service) 'lowdelay' (for instance interactive ssh sessions) and TCP ACKs without payload.

Both incoming and outgoing TCP connections will pass by those two rules, create state, and all packets related to the connections will be assigned to either the q_def or q_pri queues. Packets assigned to the q_pri queue will have priority and will get sent before any pending packets in the q_def queue.


The following test was performed first without and then with the ALTQ rules explained above:

  • -10 to -8 minutes: idle
  • -8 to -6 minutes: download only
  • -6 to -4 minutes: concurrent download and upload
  • -4 to -2 minutes: upload only
  • -2 to 0 minutes: idle

The first graphs shows the results of the test without ALTQ, and the second one with ALTQ:

[without altq]
[with altq]

The improvement is quite significant, the saturated uplink no longer delays the outgoing empty ACKs, and the download rate doesn't drop anymore.

This effect is not limited to asymmetric links, it occurs whenever one direction of the link is saturated. With an asymmetric link this occurs more often, obviously.


OpenBSD 3.3-release didn't support queueing on the tun(4) interface. This has since been fixed, see altq on tun for a description and results.

Related links

Last updated on Tue Sep 26 08:57:43 2017 by daniel@benzedrine.ch.