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:
| Scope | Syntax | Lifetime |
|---|---|---|
| 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
maxcallsinasterisk.confto 110% of your expected peak. Without it, Asterisk accepts calls until memory exhausts. - Enable
jbenable=yesinrtp.confwithjbmaxsize=200for calls crossing WAN links with variable latency. - Use
PJSIP(chan_pjsip) instead ofchan_sip— chan_sip is deprecated since Asterisk 17 and removed in 21. - Set
timers=yesin pjsip endpoints to send SIP OPTIONS keepalives and detect dead trunks within 30 seconds. - Pre-compile patterns with
core set debug 0in 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.




