[{"data":1,"prerenderedAt":818},["ShallowReactive",2],{"post-\u002Fblog\u002Fasterisk-dialplan-best-practices":3},{"id":4,"title":5,"author":6,"body":7,"category":802,"coverImage":803,"date":804,"description":805,"extension":806,"meta":807,"navigation":65,"path":808,"readingTime":93,"seo":809,"stem":810,"tags":811,"__hash__":817},"posts\u002Fblog\u002Fasterisk-dialplan-best-practices.md","Asterisk Dialplan Best Practices for Production Systems","Tumarm Engineering",{"type":8,"value":9,"toc":790},"minimark",[10,14,18,23,31,191,196,199,238,245,249,263,306,325,329,350,412,419,423,426,441,444,534,541,545,548,610,627,631,646,649,713,716,720,773,777,786],[11,12,5],"h1",{"id":13},"asterisk-dialplan-best-practices-for-production-systems",[15,16,17],"p",{},"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.",[19,20,22],"h2",{"id":21},"context-and-pattern-organization","Context and Pattern Organization",[15,24,25,26,30],{},"Every context is a namespace. Keep them small and purposeful. A common mistake is one monolithic ",[27,28,29],"code",{},"[from-internal]"," context that grows to 500 lines. Split by function:",[32,33,38],"pre",{"className":34,"code":35,"language":36,"meta":37,"style":37},"language-ini shiki shiki-themes github-light github-dark","[globals]\nTRUNK_OUTBOUND=PJSIP\u002Fcarrier-trunk\nLOG_LEVEL=3\n\n[from-pstn]\n; Inbound from carrier — validate and route only\nexten => _+1NXXXXXXXXX,1,NoOp(Inbound DID: ${EXTEN})\n same => n,Set(DID=${EXTEN})\n same => n,GoSub(sub-did-lookup,s,1(${DID}))\n same => n,Goto(ivr-main,s,1)\n same => n,Hangup()\n\n[from-internal]\n; Authenticated SIP endpoints only\nexten => _NXXNXXXXXX,1,GoSub(sub-dialout,s,1(${EXTEN}))\nexten => _011.,1,GoSub(sub-international,s,1(${EXTEN}))\nexten => *98,1,VoiceMailMain(${CALLERID(num)}@default)\n\n[sub-dialout]\n; Reusable outbound dialing subroutine\nexten => s,1,NoOp(Dialing: ${ARG1})\n same => n,Set(CALLERID(num)=${OUTBOUND_CID})\n same => n,Dial(${TRUNK_OUTBOUND}\u002F${ARG1},30,gTt)\n same => n,GoSub(sub-handle-dialstatus,s,1)\n same => n,Return()\n","ini","",[27,39,40,48,54,60,67,73,79,85,91,97,103,109,114,120,126,132,138,144,149,155,161,167,173,179,185],{"__ignoreMap":37},[41,42,45],"span",{"class":43,"line":44},"line",1,[41,46,47],{},"[globals]\n",[41,49,51],{"class":43,"line":50},2,[41,52,53],{},"TRUNK_OUTBOUND=PJSIP\u002Fcarrier-trunk\n",[41,55,57],{"class":43,"line":56},3,[41,58,59],{},"LOG_LEVEL=3\n",[41,61,63],{"class":43,"line":62},4,[41,64,66],{"emptyLinePlaceholder":65},true,"\n",[41,68,70],{"class":43,"line":69},5,[41,71,72],{},"[from-pstn]\n",[41,74,76],{"class":43,"line":75},6,[41,77,78],{},"; Inbound from carrier — validate and route only\n",[41,80,82],{"class":43,"line":81},7,[41,83,84],{},"exten => _+1NXXXXXXXXX,1,NoOp(Inbound DID: ${EXTEN})\n",[41,86,88],{"class":43,"line":87},8,[41,89,90],{}," same => n,Set(DID=${EXTEN})\n",[41,92,94],{"class":43,"line":93},9,[41,95,96],{}," same => n,GoSub(sub-did-lookup,s,1(${DID}))\n",[41,98,100],{"class":43,"line":99},10,[41,101,102],{}," same => n,Goto(ivr-main,s,1)\n",[41,104,106],{"class":43,"line":105},11,[41,107,108],{}," same => n,Hangup()\n",[41,110,112],{"class":43,"line":111},12,[41,113,66],{"emptyLinePlaceholder":65},[41,115,117],{"class":43,"line":116},13,[41,118,119],{},"[from-internal]\n",[41,121,123],{"class":43,"line":122},14,[41,124,125],{},"; Authenticated SIP endpoints only\n",[41,127,129],{"class":43,"line":128},15,[41,130,131],{},"exten => _NXXNXXXXXX,1,GoSub(sub-dialout,s,1(${EXTEN}))\n",[41,133,135],{"class":43,"line":134},16,[41,136,137],{},"exten => _011.,1,GoSub(sub-international,s,1(${EXTEN}))\n",[41,139,141],{"class":43,"line":140},17,[41,142,143],{},"exten => *98,1,VoiceMailMain(${CALLERID(num)}@default)\n",[41,145,147],{"class":43,"line":146},18,[41,148,66],{"emptyLinePlaceholder":65},[41,150,152],{"class":43,"line":151},19,[41,153,154],{},"[sub-dialout]\n",[41,156,158],{"class":43,"line":157},20,[41,159,160],{},"; Reusable outbound dialing subroutine\n",[41,162,164],{"class":43,"line":163},21,[41,165,166],{},"exten => s,1,NoOp(Dialing: ${ARG1})\n",[41,168,170],{"class":43,"line":169},22,[41,171,172],{}," same => n,Set(CALLERID(num)=${OUTBOUND_CID})\n",[41,174,176],{"class":43,"line":175},23,[41,177,178],{}," same => n,Dial(${TRUNK_OUTBOUND}\u002F${ARG1},30,gTt)\n",[41,180,182],{"class":43,"line":181},24,[41,183,184],{}," same => n,GoSub(sub-handle-dialstatus,s,1)\n",[41,186,188],{"class":43,"line":187},25,[41,189,190],{}," same => n,Return()\n",[192,193,195],"h3",{"id":194},"pattern-matching-priority","Pattern Matching Priority",[15,197,198],{},"Asterisk matches extensions by specificity, not order. Longer patterns beat shorter ones. Exact matches beat patterns. This trips teams up with overlapping international prefixes:",[32,200,202],{"className":34,"code":201,"language":36,"meta":37,"style":37},"; These DO NOT conflict — exact beats pattern\nexten => 011972,1,NoOp(Israel direct)\nexten => _011.,1,NoOp(Generic international)\n\n; These DO conflict — same pattern length, Asterisk picks first loaded\nexten => _NXXXXXXXXX,1,NoOp(10-digit US)\nexten => _1XXXXXXXXX,1,NoOp(1+ US)   ; never reached for 1XXXXXXXXX\n",[27,203,204,209,214,219,223,228,233],{"__ignoreMap":37},[41,205,206],{"class":43,"line":44},[41,207,208],{},"; These DO NOT conflict — exact beats pattern\n",[41,210,211],{"class":43,"line":50},[41,212,213],{},"exten => 011972,1,NoOp(Israel direct)\n",[41,215,216],{"class":43,"line":56},[41,217,218],{},"exten => _011.,1,NoOp(Generic international)\n",[41,220,221],{"class":43,"line":62},[41,222,66],{"emptyLinePlaceholder":65},[41,224,225],{"class":43,"line":69},[41,226,227],{},"; These DO conflict — same pattern length, Asterisk picks first loaded\n",[41,229,230],{"class":43,"line":75},[41,231,232],{},"exten => _NXXXXXXXXX,1,NoOp(10-digit US)\n",[41,234,235],{"class":43,"line":81},[41,236,237],{},"exten => _1XXXXXXXXX,1,NoOp(1+ US)   ; never reached for 1XXXXXXXXX\n",[15,239,240,241,244],{},"Run ",[27,242,243],{},"dialplan show \u003Ccontext>"," after every non-trivial change to see the compiled pattern tree.",[19,246,248],{"id":247},"gosub-over-macro","GoSub Over Macro",[15,250,251,252,255,256,259,260,262],{},"Asterisk deprecated the ",[27,253,254],{},"Macro"," application in 16.x and removed it in 21.x. Use ",[27,257,258],{},"GoSub"," for all reusable logic. The key difference: ",[27,261,258],{}," 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.",[32,264,266],{"className":34,"code":265,"language":36,"meta":37,"style":37},"; Correct: GoSub with arguments\nexten => s,1,GoSub(sub-playback,s,1(ivr-welcome,en))\n\n[sub-playback]\nexten => s,1,NoOp(Playing ${ARG1} in ${ARG2})\n same => n,Background(${ARG1})\n same => n,WaitExten(5)\n same => n,Return()\n",[27,267,268,273,278,282,287,292,297,302],{"__ignoreMap":37},[41,269,270],{"class":43,"line":44},[41,271,272],{},"; Correct: GoSub with arguments\n",[41,274,275],{"class":43,"line":50},[41,276,277],{},"exten => s,1,GoSub(sub-playback,s,1(ivr-welcome,en))\n",[41,279,280],{"class":43,"line":56},[41,281,66],{"emptyLinePlaceholder":65},[41,283,284],{"class":43,"line":62},[41,285,286],{},"[sub-playback]\n",[41,288,289],{"class":43,"line":69},[41,290,291],{},"exten => s,1,NoOp(Playing ${ARG1} in ${ARG2})\n",[41,293,294],{"class":43,"line":75},[41,295,296],{}," same => n,Background(${ARG1})\n",[41,298,299],{"class":43,"line":81},[41,300,301],{}," same => n,WaitExten(5)\n",[41,303,304],{"class":43,"line":87},[41,305,190],{},[15,307,308,309,312,313,316,317,320,321,324],{},"Arguments are ",[27,310,311],{},"${ARG1}",", ",[27,314,315],{},"${ARG2}",", etc. They do not leak into the calling context. Local variables set inside a subroutine with ",[27,318,319],{},"Set(LOCAL(var)=value)"," are stack-scoped and cleaned up on ",[27,322,323],{},"Return()",".",[19,326,328],{"id":327},"handling-hangup-correctly","Handling Hangup Correctly",[15,330,331,332,335,336,312,339,312,342,345,346,349],{},"Unhandled hangups are the most common production dialplan bug. When a caller hangs up mid-call, Asterisk throws a ",[27,333,334],{},"HANGUP"," signal. If your dialplan has blocking applications (",[27,337,338],{},"WaitExten",[27,340,341],{},"Record",[27,343,344],{},"AGI","), they exit immediately and Asterisk executes the ",[27,347,348],{},"h"," extension in the current context.",[32,351,353],{"className":34,"code":352,"language":36,"meta":37,"style":37},"[ivr-main]\nexten => s,1,Answer()\n same => n,Background(welcome-message)\n same => n,WaitExten(10)\n same => n,Goto(ivr-main,timeout,1)\n\nexten => timeout,1,Playback(please-try-again)\n same => n,Goto(ivr-main,s,1)\n\n; Always define h — even if just to log\nexten => h,1,NoOp(Hangup in ivr-main for ${CALLERID(num)})\n same => n,GoSub(sub-cdr-close,s,1)\n",[27,354,355,360,365,370,375,380,384,389,393,397,402,407],{"__ignoreMap":37},[41,356,357],{"class":43,"line":44},[41,358,359],{},"[ivr-main]\n",[41,361,362],{"class":43,"line":50},[41,363,364],{},"exten => s,1,Answer()\n",[41,366,367],{"class":43,"line":56},[41,368,369],{}," same => n,Background(welcome-message)\n",[41,371,372],{"class":43,"line":62},[41,373,374],{}," same => n,WaitExten(10)\n",[41,376,377],{"class":43,"line":69},[41,378,379],{}," same => n,Goto(ivr-main,timeout,1)\n",[41,381,382],{"class":43,"line":75},[41,383,66],{"emptyLinePlaceholder":65},[41,385,386],{"class":43,"line":81},[41,387,388],{},"exten => timeout,1,Playback(please-try-again)\n",[41,390,391],{"class":43,"line":87},[41,392,102],{},[41,394,395],{"class":43,"line":93},[41,396,66],{"emptyLinePlaceholder":65},[41,398,399],{"class":43,"line":99},[41,400,401],{},"; Always define h — even if just to log\n",[41,403,404],{"class":43,"line":105},[41,405,406],{},"exten => h,1,NoOp(Hangup in ivr-main for ${CALLERID(num)})\n",[41,408,409],{"class":43,"line":111},[41,410,411],{}," same => n,GoSub(sub-cdr-close,s,1)\n",[15,413,414,415,418],{},"Without ",[27,416,417],{},"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.",[19,420,422],{"id":421},"agi-integration-patterns","AGI Integration Patterns",[15,424,425],{},"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.",[32,427,429],{"className":34,"code":428,"language":36,"meta":37,"style":37},"exten => s,1,AGI(agi:\u002F\u002F127.0.0.1:4573\u002Froute-call)\n same => n,Goto(${AGIRESULT},1)\n",[27,430,431,436],{"__ignoreMap":37},[41,432,433],{"class":43,"line":44},[41,434,435],{},"exten => s,1,AGI(agi:\u002F\u002F127.0.0.1:4573\u002Froute-call)\n",[41,437,438],{"class":43,"line":50},[41,439,440],{}," same => n,Goto(${AGIRESULT},1)\n",[15,442,443],{},"Your FastAGI server listens on port 4573, receives the AGI environment variables, and responds with AGI commands:",[32,445,449],{"className":446,"code":447,"language":448,"meta":37,"style":37},"language-python shiki shiki-themes github-light github-dark","# Minimal FastAGI handler (Python)\nimport socket, sys\n\ndef handle(conn):\n    f = conn.makefile()\n    env = {}\n    while True:\n        line = f.readline().strip()\n        if not line:\n            break\n        key, _, val = line.partition(': ')\n        env[key] = val\n\n    # Send AGI command\n    conn.sendall(b'SET VARIABLE AGIRESULT \"routed-context\"\\n')\n    response = f.readline()  # 200 result=1\n    conn.sendall(b'HANGUP\\n')\n","python",[27,450,451,456,461,465,470,475,480,485,490,495,500,505,510,514,519,524,529],{"__ignoreMap":37},[41,452,453],{"class":43,"line":44},[41,454,455],{},"# Minimal FastAGI handler (Python)\n",[41,457,458],{"class":43,"line":50},[41,459,460],{},"import socket, sys\n",[41,462,463],{"class":43,"line":56},[41,464,66],{"emptyLinePlaceholder":65},[41,466,467],{"class":43,"line":62},[41,468,469],{},"def handle(conn):\n",[41,471,472],{"class":43,"line":69},[41,473,474],{},"    f = conn.makefile()\n",[41,476,477],{"class":43,"line":75},[41,478,479],{},"    env = {}\n",[41,481,482],{"class":43,"line":81},[41,483,484],{},"    while True:\n",[41,486,487],{"class":43,"line":87},[41,488,489],{},"        line = f.readline().strip()\n",[41,491,492],{"class":43,"line":93},[41,493,494],{},"        if not line:\n",[41,496,497],{"class":43,"line":99},[41,498,499],{},"            break\n",[41,501,502],{"class":43,"line":105},[41,503,504],{},"        key, _, val = line.partition(': ')\n",[41,506,507],{"class":43,"line":111},[41,508,509],{},"        env[key] = val\n",[41,511,512],{"class":43,"line":116},[41,513,66],{"emptyLinePlaceholder":65},[41,515,516],{"class":43,"line":122},[41,517,518],{},"    # Send AGI command\n",[41,520,521],{"class":43,"line":128},[41,522,523],{},"    conn.sendall(b'SET VARIABLE AGIRESULT \"routed-context\"\\n')\n",[41,525,526],{"class":43,"line":134},[41,527,528],{},"    response = f.readline()  # 200 result=1\n",[41,530,531],{"class":43,"line":140},[41,532,533],{},"    conn.sendall(b'HANGUP\\n')\n",[15,535,536,537,540],{},"FastAGI responses must come within the ",[27,538,539],{},"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.",[19,542,544],{"id":543},"variable-scoping-and-channel-variables","Variable Scoping and Channel Variables",[15,546,547],{},"Asterisk has three variable scopes:",[549,550,551,567],"table",{},[552,553,554],"thead",{},[555,556,557,561,564],"tr",{},[558,559,560],"th",{},"Scope",[558,562,563],{},"Syntax",[558,565,566],{},"Lifetime",[568,569,570,584,597],"tbody",{},[555,571,572,576,581],{},[573,574,575],"td",{},"Channel",[573,577,578],{},[27,579,580],{},"${VAR}",[573,582,583],{},"Single channel, dies on hangup",[555,585,586,589,594],{},[573,587,588],{},"Global",[573,590,591],{},[27,592,593],{},"${GLOBAL(VAR)}",[573,595,596],{},"Process lifetime, shared across calls",[555,598,599,602,607],{},[573,600,601],{},"Subroutine local",[573,603,604],{},[27,605,606],{},"${LOCAL(VAR)}",[573,608,609],{},"Stack frame only",[15,611,612,613,616,617,620,621,624,625,324],{},"A common bug: setting a variable in a subroutine with ",[27,614,615],{},"Set(VAR=value)"," instead of ",[27,618,619],{},"Set(LOCAL(VAR)=value)",". The former leaks into the calling context and overwrites values you didn't intend to change. Use ",[27,622,623],{},"LOCAL()"," for any variable that should not persist after ",[27,626,323],{},[19,628,630],{"id":629},"logging-and-debugging-in-production","Logging and Debugging in Production",[15,632,633,634,637,638,641,642,645],{},"Never rely on ",[27,635,636],{},"verbose"," logging in production — it floods the log file and impacts performance at high concurrency. Use ",[27,639,640],{},"NoOp"," sparingly and structured logging via the ",[27,643,644],{},"CEL"," (Channel Event Logging) subsystem instead.",[15,647,648],{},"Configure CEL to write to a PostgreSQL or MySQL backend:",[32,650,652],{"className":34,"code":651,"language":36,"meta":37,"style":37},"; \u002Fetc\u002Fasterisk\u002Fcel.conf\n[general]\nenable=yes\napps=all\nevents=CHANNEL_START,CHANNEL_END,ANSWER,HANGUP,BRIDGE_ENTER,BRIDGE_EXIT\n\n; \u002Fetc\u002Fasterisk\u002Fcel_pgsql.conf\n[global]\nhostname=localhost\ndbname=asterisk_cdr\npassword=secret\ntable=cel\n",[27,653,654,659,664,669,674,679,683,688,693,698,703,708],{"__ignoreMap":37},[41,655,656],{"class":43,"line":44},[41,657,658],{},"; \u002Fetc\u002Fasterisk\u002Fcel.conf\n",[41,660,661],{"class":43,"line":50},[41,662,663],{},"[general]\n",[41,665,666],{"class":43,"line":56},[41,667,668],{},"enable=yes\n",[41,670,671],{"class":43,"line":62},[41,672,673],{},"apps=all\n",[41,675,676],{"class":43,"line":69},[41,677,678],{},"events=CHANNEL_START,CHANNEL_END,ANSWER,HANGUP,BRIDGE_ENTER,BRIDGE_EXIT\n",[41,680,681],{"class":43,"line":75},[41,682,66],{"emptyLinePlaceholder":65},[41,684,685],{"class":43,"line":81},[41,686,687],{},"; \u002Fetc\u002Fasterisk\u002Fcel_pgsql.conf\n",[41,689,690],{"class":43,"line":87},[41,691,692],{},"[global]\n",[41,694,695],{"class":43,"line":93},[41,696,697],{},"hostname=localhost\n",[41,699,700],{"class":43,"line":99},[41,701,702],{},"dbname=asterisk_cdr\n",[41,704,705],{"class":43,"line":105},[41,706,707],{},"password=secret\n",[41,709,710],{"class":43,"line":111},[41,711,712],{},"table=cel\n",[15,714,715],{},"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.",[19,717,719],{"id":718},"performance-checklist-for-production","Performance Checklist for Production",[721,722,723,735,749,760,766],"ul",{},[724,725,726,727,730,731,734],"li",{},"Set ",[27,728,729],{},"maxcalls"," in ",[27,732,733],{},"asterisk.conf"," to 110% of your expected peak. Without it, Asterisk accepts calls until memory exhausts.",[724,736,737,738,730,741,744,745,748],{},"Enable ",[27,739,740],{},"jbenable=yes",[27,742,743],{},"rtp.conf"," with ",[27,746,747],{},"jbmaxsize=200"," for calls crossing WAN links with variable latency.",[724,750,751,752,755,756,759],{},"Use ",[27,753,754],{},"PJSIP"," (chan_pjsip) instead of ",[27,757,758],{},"chan_sip"," — chan_sip is deprecated since Asterisk 17 and removed in 21.",[724,761,726,762,765],{},[27,763,764],{},"timers=yes"," in pjsip endpoints to send SIP OPTIONS keepalives and detect dead trunks within 30 seconds.",[724,767,768,769,772],{},"Pre-compile patterns with ",[27,770,771],{},"core set debug 0"," in production — debug mode disables some compiler optimizations in the pattern matcher.",[19,774,776],{"id":775},"putting-it-together","Putting It Together",[15,778,779,780,782,783,785],{},"A production Asterisk dialplan is not a script — it's a state machine where every transition must be explicit. Define the ",[27,781,348],{}," extension everywhere. Use ",[27,784,258],{}," 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.",[787,788,789],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":37,"searchDepth":50,"depth":50,"links":791},[792,795,796,797,798,799,800,801],{"id":21,"depth":50,"text":22,"children":793},[794],{"id":194,"depth":56,"text":195},{"id":247,"depth":50,"text":248},{"id":327,"depth":50,"text":328},{"id":421,"depth":50,"text":422},{"id":543,"depth":50,"text":544},{"id":629,"depth":50,"text":630},{"id":718,"depth":50,"text":719},{"id":775,"depth":50,"text":776},"SIP","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1629654297299-c8506221ca97?w=1200&q=80","2025-07-01","Production-grade Asterisk dialplan patterns: extension priority hygiene, macro vs GoSub, AGI integration, logging strategies, and avoiding common performance pitfalls.","md",{},"\u002Fblog\u002Fasterisk-dialplan-best-practices",{"title":5,"description":805},"blog\u002Fasterisk-dialplan-best-practices",[812,813,814,815,816],"asterisk","dialplan","agi","pbx","sip","gPGNrB7reSO_I4DA26_IdENLXmCdISfNHBFBzaqcp98",1776964996445]