Skip to content

fix(transaction): non-INVITE client transaction waits Timer K, not Timer D - #358

Open
alexvrf wants to merge 1 commit into
emiago:mainfrom
alexvrf:fix/non-invite-timer-k
Open

alexvrf wants to merge 1 commit into
emiago:mainfrom
alexvrf:fix/non-invite-timer-k

Conversation

@alexvrf

@alexvrf alexvrf commented Sep 7, 2026

Copy link
Copy Markdown

Problem

A client transaction lingers in Completed to absorb retransmitted final
responses. RFC 3261 gives the two transaction kinds different timers for
that wait:

  • 17.1.1.2 — an INVITE client transaction uses Timer D, "at least
    32 seconds for unreliable transports";
  • 17.1.2.2 — a non-INVITE client transaction uses Timer K, which
    is T4 for unreliable transports.

ClientTx.Init sets timer_d_time = Timer_D for both kinds, so every
OPTIONS, BYE, INFO and REGISTER transaction stays in the
transaction layer roughly six times longer than the RFC asks for (32s
against the default T4 of 5s).

Nothing wrong happens on the wire — the transaction simply absorbs
retransmissions longer than it has to — but the transaction table grows.
It shows up on a proxy or B2BUA that keep-alives its peers with OPTIONS:
with one probe per peer every few seconds, thousands of transactions that
should already be gone are still held.

Timer_K is already declared and populated by SetTimers; before this
change it was the only timer of the set that nothing ever read.

Fix

Pick the wait by the method of the request the transaction was created
for:

if tx.origin.IsInvite() {
    tx.timer_d_time = Timer_D
} else {
    tx.timer_d_time = Timer_K
}

The reliable-transport branch is untouched: there the wait is 0 for both
kinds, which is what 17.1.1.2 and 17.1.2.2 say.

Test

TestClientTxCompletedWaitPerMethod builds a client transaction for each
of four cases and checks the wait it got:

Request Transport Wait
INVITE UDP Timer_D
OPTIONS UDP Timer_K
INVITE TCP 0
OPTIONS TCP 0

The TCP rows are there because the change is confined to the unreliable
branch, and that is worth holding: a later change must not start arming a
timer over a reliable transport. On the current code the OPTIONS-over-UDP
row fails.

Compatibility

No public API change. Behaviour changes only in how long a terminated
non-INVITE client transaction is retained, and only towards what the RFC
prescribes.

…mer D

A client transaction lingers in Completed to absorb retransmitted final
responses. RFC 3261 17.1.1.2 gives an INVITE client transaction Timer D —
32 seconds on unreliable transports; 17.1.2.2 gives a non-INVITE one
Timer K, which is T4.

Init() set timer_d_time to Timer_D for both, so every OPTIONS, BYE, INFO
and REGISTER transaction stayed in the transaction layer roughly six times
longer than the RFC asks for. The behaviour is not wrong on the wire — the
transaction simply absorbs retransmissions longer than it has to — but the
table grows: a proxy or B2BUA that keep-alives many peers with OPTIONS
holds thousands of terminated-but-not-removed transactions.

Timer_K was already declared and populated by SetTimers, and was the only
timer of the set never read.

TestClientTxCompletedWaitPerMethod covers both kinds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants