Skip to content
Asterisk Dialplan Best Practices for Production Systems
SIP

Asterisk Dialplan Best Practices for Production Systems

Production-grade Asterisk dialplan patterns: extension priority hygiene, macro vs GoSub, AGI integration, logging strategies, and avoiding common performance pitfalls.

Tumarm Engineering9 min read

Asterisk Dialplan Best Practices for Production Systems

A poorly structured Asterisk dialplan works fine with five extensions and breaks under production load in ways that are hard to debug: pattern match collisions, unhandled hangups, silent audio drops, and priority gaps that fall through to the wrong context. This post covers dialplan patterns that hold up at scale — carrier SIP trunks, multi-tenant IVR, high-concurrency outbound dialing — based on running Asterisk in production environments with hundreds of simultaneous calls.

Context and Pattern Organization

Every context is a namespace. Keep them small and purposeful. A common mistake is one monolithic [from-internal] context that grows to 500 lines. Split by function:

[globals]
TRUNK_OUTBOUND=PJSIP/carrier-trunk
LOG_LEVEL=3

[from-pstn]
; Inbound from carrier — validate and route only
exten => _+1NXXXXXXXXX,1,NoOp(Inbound DID: ${EXTEN})
 same => n,Set(DID=${EXTEN})
 same => n,GoSub(sub-did-lookup,s,1(${DID}))
 same => n,Goto(ivr-main,s,1)
 same => n,Hangup()

[from-internal]
; Authenticated SIP endpoints only
exten => _NXXNXXXXXX,1,GoSub(sub-dialout,s,1(${EXTEN}))
exten => _011.,1,GoSub(sub-international,s,1(${EXTEN}))
exten => *98,1,VoiceMailMain(${CALLERID(num)}@default)

[sub-dialout]
; Reusable outbound dialing subroutine
exten => s,1,NoOp(Dialing: ${ARG1})
 same => n,Set(CALLERID(num)=${OUTBOUND_CID})
 same => n,Dial(${TRUNK_OUTBOUND}/${ARG1},30,gTt)
 same => n,GoSub(sub-handle-dialstatus,s,1)
 same => n,Return()

Pattern Matching Priority

Asterisk matches extensions by specificity, not order. Longer patterns beat shorter ones. Exact matches beat patterns. This trips teams up with overlapping international prefixes:

; These DO NOT conflict — exact beats pattern
exten => 011972,1,NoOp(Israel direct)
exten => _011.,1,NoOp(Generic international)

; These DO conflict — same pattern length, Asterisk picks first loaded
exten => _NXXXXXXXXX,1,NoOp(10-digit US)
exten => _1XXXXXXXXX,1,NoOp(1+ US)   ; never reached for 1XXXXXXXXX

Run dialplan show <context> after every non-trivial change to see the compiled pattern tree.

GoSub Over Macro

Asterisk deprecated the Macro application in 16.x and removed it in 21.x. Use GoSub for all reusable logic. The key difference: GoSub uses a proper call stack, so nested subroutines work correctly. Macros shared a flat variable namespace and had stack depth limits that caused subtle bugs under recursion.

; Correct: GoSub with arguments
exten => s,1,GoSub(sub-playback,s,1(ivr-welcome,en))

[sub-playback]
exten => s,1,NoOp(Playing ${ARG1} in ${ARG2})
 same => n,Background(${ARG1})
 same => n,WaitExten(5)
 same => n,Return()

Arguments are ${ARG1}, ${ARG2}, etc. They do not leak into the calling context. Local variables set inside a subroutine with Set(LOCAL(var)=value) are stack-scoped and cleaned up on Return().

Handling Hangup Correctly

Unhandled hangups are the most common production dialplan bug. When a caller hangs up mid-call, Asterisk throws a HANGUP signal. If your dialplan has blocking applications (WaitExten, Record, AGI), they exit immediately and Asterisk executes the h extension in the current context.

[ivr-main]
exten => s,1,Answer()
 same => n,Background(welcome-message)
 same => n,WaitExten(10)
 same => n,Goto(ivr-main,timeout,1)

exten => timeout,1,Playback(please-try-again)
 same => n,Goto(ivr-main,s,1)

; Always define h — even if just to log
exten => h,1,NoOp(Hangup in ivr-main for ${CALLERID(num)})
 same => n,GoSub(sub-cdr-close,s,1)

Without exten => h, Asterisk logs a warning and the call ends silently — fine in dev, maddening in production when you're trying to trace dropped calls.

AGI Integration Patterns

AGI (Asterisk Gateway Interface) lets you drive dialplan logic from an external process. Use FastAGI over a persistent TCP socket rather than launching a new process per call — process spawn overhead adds 30–80ms per call under load.

exten => s,1,AGI(agi://127.0.0.1:4573/route-call)
 same => n,Goto(${AGIRESULT},1)

Your FastAGI server listens on port 4573, receives the AGI environment variables, and responds with AGI commands:

# Minimal FastAGI handler (Python)
import socket, sys

def handle(conn):
    f = conn.makefile()
    env = {}
    while True:
        line = f.readline().strip()
        if not line:
            break
        key, _, val = line.partition(': ')
        env[key] = val

    # Send AGI command
    conn.sendall(b'SET VARIABLE AGIRESULT "routed-context"\n')
    response = f.readline()  # 200 result=1
    conn.sendall(b'HANGUP\n')

FastAGI responses must come within the agitimeout setting (default 10 seconds). If your AGI handler queries a database, keep connection pools warm — cold connection latency shows up directly as call setup delay.

Variable Scoping and Channel Variables

Asterisk has three variable scopes:

ScopeSyntaxLifetime
Channel${VAR}Single channel, dies on hangup
Global${GLOBAL(VAR)}Process lifetime, shared across calls
Subroutine local${LOCAL(VAR)}Stack frame only

A common bug: setting a variable in a subroutine with Set(VAR=value) instead of Set(LOCAL(VAR)=value). The former leaks into the calling context and overwrites values you didn't intend to change. Use LOCAL() for any variable that should not persist after Return().

Logging and Debugging in Production

Never rely on verbose logging in production — it floods the log file and impacts performance at high concurrency. Use NoOp sparingly and structured logging via the CEL (Channel Event Logging) subsystem instead.

Configure CEL to write to a PostgreSQL or MySQL backend:

; /etc/asterisk/cel.conf
[general]
enable=yes
apps=all
events=CHANNEL_START,CHANNEL_END,ANSWER,HANGUP,BRIDGE_ENTER,BRIDGE_EXIT

; /etc/asterisk/cel_pgsql.conf
[global]
hostname=localhost
dbname=asterisk_cdr
password=secret
table=cel

CEL gives you per-call event streams with microsecond timestamps. Combined with a Grafana dashboard querying the CEL table, you get real-time visibility into call flows without parsing log files.

Performance Checklist for Production

  • Set maxcalls in asterisk.conf to 110% of your expected peak. Without it, Asterisk accepts calls until memory exhausts.
  • Enable jbenable=yes in rtp.conf with jbmaxsize=200 for calls crossing WAN links with variable latency.
  • Use PJSIP (chan_pjsip) instead of chan_sip — chan_sip is deprecated since Asterisk 17 and removed in 21.
  • Set timers=yes in pjsip endpoints to send SIP OPTIONS keepalives and detect dead trunks within 30 seconds.
  • Pre-compile patterns with core set debug 0 in production — debug mode disables some compiler optimizations in the pattern matcher.

Putting It Together

A production Asterisk dialplan is not a script — it's a state machine where every transition must be explicit. Define the h extension everywhere. Use GoSub for reuse. Drive dynamic logic from FastAGI with warm connection pools. Log via CEL, not verbose. These practices eliminate 80% of the "mysterious call drop" tickets that haunt Asterisk deployments.

asteriskdialplanagipbxsip
Benchmark
BALI Pvt.Ltd
Brave BPO
Wave
SmartBrains BPO

Ready to build on carrier-grade voice?

Talk to a VoIP engineer — not a salesperson.

Schedule a Technical Call →