Server-side echo cancellation on PSTN audio — advice, delay limits, and recommended config?

Context up front: we’re not on LiveKit Cloud or rooms — we use the livekit Python package standalone for its AudioProcessingModule, doing server-side echo cancellation in a phone-bot pipeline (Twilio Media Streams, PSTN callers, 8 kHz).

Goal: cancel the caller’s speakerphone echo of our bot’s TTS server-side, before VAD/STT — PSTN callers have no client-side WebRTC AEC, and the leaked echo gets transcribed and triggers barge-in, so the bot interrupts itself.

What happens: APM works well when the echo path is short. But in our setup the echo returns 1–2 s (variable) after we feed the reference — outbound audio is buffered by Twilio and the carrier — and AEC3 then fails silently: ~0 dB reduction, no error. set_stream_delay_ms shifts the covered band but can’t span a 1–2 s variable delay.

Environment/versions: livekit 1.1.13 (pip), Python 3.12, Linux x86-64, 10 ms int16 frames at 8 kHz both streams. Usage skeleton:

apm = rtc.AudioProcessingModule(echo_cancellation=True, noise_suppression=False,
                                high_pass_filter=False, auto_gain_control=False)
apm.set_stream_delay_ms(100)
# on each outbound 10ms frame (as written to Twilio):
apm.process_reverse_stream(ref_frame)
# on each inbound 10ms frame (from Twilio):
apm.process_stream(in_frame)

Questions:

  1. Does anyone have tips for better usage of AEC3 for this use case?
  2. Is there an altogether different echo cancellation solution you’d suggest?
  3. Is there any way to give APM a large or dynamic delay hint? Is silent failure beyond ~500 ms the expected AEC3 behavior, and is there a recommended configuration for long, variable echo paths?
  4. How does LiveKit’s own SIP/PSTN bridge handle far-end device echo — server-side AEC3, Krisp, or is this handled at a different layer entirely?
  5. In your experience, is nonlinear residue from the far-end device’s own echo suppressor something APM can meaningfully cancel, or is the accepted answer “you can’t linearly cancel it — suppress or filter downstream instead”?