[{"data":1,"prerenderedAt":10398},["ShallowReactive",2],{"posts":3},[4,1040,2710,3509,4579,4964,5819,6206,6943,7311,8112,9167,9915],{"id":5,"title":6,"author":7,"body":8,"category":1023,"coverImage":1024,"date":1025,"description":1026,"extension":1027,"meta":1028,"navigation":94,"path":1029,"readingTime":148,"seo":1030,"stem":1031,"tags":1032,"__hash__":1039},"posts\u002Fblog\u002Frtpengine-deployment-guide.md","rtpengine Deployment Guide: RTP Proxy at Scale","Tumarm Engineering",{"type":9,"value":10,"toc":1012},"minimark",[11,15,19,24,32,43,47,229,233,374,377,400,404,627,638,642,649,657,660,768,772,775,781,784,800,806,810,813,819,826,833,839,843,904,907,910,963,967,974,999,1005,1008],[12,13,6],"h1",{"id":14},"rtpengine-deployment-guide-rtp-proxy-at-scale",[16,17,18],"p",{},"rtpengine is the RTP proxy of choice for production SIP infrastructure. It replaces the older rtpproxy with kernel-space packet forwarding, SRTP\u002FDTLS support, codec transcoding, and a gRPC\u002Fng control protocol that Kamailio and OpenSIPS integrate natively. At scale — carrier interconnects, WebRTC gateways, SBCs — rtpengine handles tens of thousands of concurrent RTP sessions on commodity hardware. This guide covers installation, kernel module setup, Kamailio integration, and the operational details that documentation skips.",[20,21,23],"h2",{"id":22},"why-rtpengine-over-alternatives","Why rtpengine Over Alternatives",[16,25,26,27,31],{},"The core advantage is the kernel forwarding module (",[28,29,30],"code",{},"xt_RTPENGINE","). When a session is established, rtpengine installs packet forwarding rules directly into the Linux kernel's netfilter framework. Subsequent RTP packets are forwarded in kernel space without a context switch to userspace — effectively wire-speed forwarding at the cost of a single netfilter lookup.",[16,33,34,35,38,39,42],{},"Without the kernel module, rtpengine falls back to userspace forwarding: each RTP packet traverses ",[28,36,37],{},"recvmsg()"," → userspace → ",[28,40,41],{},"sendmsg()",". This is still fast (~1 µs per packet on modern hardware) but limits throughput to roughly 300,000 packets\u002Fsec per core. With the kernel module, a 4-core server forwards over 2 million packets\u002Fsec — enough for 20,000+ concurrent audio sessions.",[20,44,46],{"id":45},"installation","Installation",[48,49,54],"pre",{"className":50,"code":51,"language":52,"meta":53,"style":53},"language-bash shiki shiki-themes github-light github-dark","# Debian \u002F Ubuntu — use the official Sipwise package repo\necho \"deb http:\u002F\u002Fpackages.sipwise.com\u002Fspce bookworm main\" \\\n    > \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fsipwise.list\n\napt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0x... # Sipwise key\napt-get update\napt-get install ngcp-rtpengine\n\n# Or build from source (for custom transcoding codecs)\napt-get install build-essential pkg-config libssl-dev libpcre3-dev \\\n    libjson-glib-dev libcurl4-openssl-dev libxmlrpc-c3-dev libglib2.0-dev\n\ngit clone https:\u002F\u002Fgithub.com\u002Fsipwise\u002Frtpengine.git\ncd rtpengine\nmake\nmake install\n","bash","",[28,55,56,65,79,89,96,121,130,141,146,152,173,188,193,205,214,220],{"__ignoreMap":53},[57,58,61],"span",{"class":59,"line":60},"line",1,[57,62,64],{"class":63},"sJ8bj","# Debian \u002F Ubuntu — use the official Sipwise package repo\n",[57,66,68,72,76],{"class":59,"line":67},2,[57,69,71],{"class":70},"sj4cs","echo",[57,73,75],{"class":74},"sZZnC"," \"deb http:\u002F\u002Fpackages.sipwise.com\u002Fspce bookworm main\"",[57,77,78],{"class":70}," \\\n",[57,80,82,86],{"class":59,"line":81},3,[57,83,85],{"class":84},"szBVR","    >",[57,87,88],{"class":74}," \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fsipwise.list\n",[57,90,92],{"class":59,"line":91},4,[57,93,95],{"emptyLinePlaceholder":94},true,"\n",[57,97,99,103,106,109,112,115,118],{"class":59,"line":98},5,[57,100,102],{"class":101},"sScJk","apt-key",[57,104,105],{"class":74}," adv",[57,107,108],{"class":70}," --keyserver",[57,110,111],{"class":74}," keyserver.ubuntu.com",[57,113,114],{"class":70}," --recv-keys",[57,116,117],{"class":74}," 0x...",[57,119,120],{"class":63}," # Sipwise key\n",[57,122,124,127],{"class":59,"line":123},6,[57,125,126],{"class":101},"apt-get",[57,128,129],{"class":74}," update\n",[57,131,133,135,138],{"class":59,"line":132},7,[57,134,126],{"class":101},[57,136,137],{"class":74}," install",[57,139,140],{"class":74}," ngcp-rtpengine\n",[57,142,144],{"class":59,"line":143},8,[57,145,95],{"emptyLinePlaceholder":94},[57,147,149],{"class":59,"line":148},9,[57,150,151],{"class":63},"# Or build from source (for custom transcoding codecs)\n",[57,153,155,157,159,162,165,168,171],{"class":59,"line":154},10,[57,156,126],{"class":101},[57,158,137],{"class":74},[57,160,161],{"class":74}," build-essential",[57,163,164],{"class":74}," pkg-config",[57,166,167],{"class":74}," libssl-dev",[57,169,170],{"class":74}," libpcre3-dev",[57,172,78],{"class":70},[57,174,176,179,182,185],{"class":59,"line":175},11,[57,177,178],{"class":74},"    libjson-glib-dev",[57,180,181],{"class":74}," libcurl4-openssl-dev",[57,183,184],{"class":74}," libxmlrpc-c3-dev",[57,186,187],{"class":74}," libglib2.0-dev\n",[57,189,191],{"class":59,"line":190},12,[57,192,95],{"emptyLinePlaceholder":94},[57,194,196,199,202],{"class":59,"line":195},13,[57,197,198],{"class":101},"git",[57,200,201],{"class":74}," clone",[57,203,204],{"class":74}," https:\u002F\u002Fgithub.com\u002Fsipwise\u002Frtpengine.git\n",[57,206,208,211],{"class":59,"line":207},14,[57,209,210],{"class":70},"cd",[57,212,213],{"class":74}," rtpengine\n",[57,215,217],{"class":59,"line":216},15,[57,218,219],{"class":101},"make\n",[57,221,223,226],{"class":59,"line":222},16,[57,224,225],{"class":101},"make",[57,227,228],{"class":74}," install\n",[20,230,232],{"id":231},"kernel-module-setup","Kernel Module Setup",[48,234,236],{"className":50,"code":235,"language":52,"meta":53,"style":53},"# Install kernel headers for your running kernel\napt-get install linux-headers-$(uname -r)\n\n# Build and load the kernel module\ncd rtpengine\u002Fkernel-module\nmake\ninsmod xt_RTPENGINE.ko\n\n# Make it persistent\ncp xt_RTPENGINE.ko \u002Flib\u002Fmodules\u002F$(uname -r)\u002Fkernel\u002Fnet\u002Fnetfilter\u002F\ndepmod -a\necho \"xt_RTPENGINE\" >> \u002Fetc\u002Fmodules\n\n# Verify it loaded\nlsmod | grep xt_RTPENGINE\n# Expected: xt_RTPENGINE    16384  0\n",[28,237,238,243,265,269,274,281,285,293,297,302,325,333,346,350,355,369],{"__ignoreMap":53},[57,239,240],{"class":59,"line":60},[57,241,242],{"class":63},"# Install kernel headers for your running kernel\n",[57,244,245,247,249,252,256,259,262],{"class":59,"line":67},[57,246,126],{"class":101},[57,248,137],{"class":74},[57,250,251],{"class":74}," linux-headers-",[57,253,255],{"class":254},"sVt8B","$(",[57,257,258],{"class":101},"uname",[57,260,261],{"class":70}," -r",[57,263,264],{"class":254},")\n",[57,266,267],{"class":59,"line":81},[57,268,95],{"emptyLinePlaceholder":94},[57,270,271],{"class":59,"line":91},[57,272,273],{"class":63},"# Build and load the kernel module\n",[57,275,276,278],{"class":59,"line":98},[57,277,210],{"class":70},[57,279,280],{"class":74}," rtpengine\u002Fkernel-module\n",[57,282,283],{"class":59,"line":123},[57,284,219],{"class":101},[57,286,287,290],{"class":59,"line":132},[57,288,289],{"class":101},"insmod",[57,291,292],{"class":74}," xt_RTPENGINE.ko\n",[57,294,295],{"class":59,"line":143},[57,296,95],{"emptyLinePlaceholder":94},[57,298,299],{"class":59,"line":148},[57,300,301],{"class":63},"# Make it persistent\n",[57,303,304,307,310,313,315,317,319,322],{"class":59,"line":154},[57,305,306],{"class":101},"cp",[57,308,309],{"class":74}," xt_RTPENGINE.ko",[57,311,312],{"class":74}," \u002Flib\u002Fmodules\u002F",[57,314,255],{"class":254},[57,316,258],{"class":101},[57,318,261],{"class":70},[57,320,321],{"class":254},")",[57,323,324],{"class":74},"\u002Fkernel\u002Fnet\u002Fnetfilter\u002F\n",[57,326,327,330],{"class":59,"line":175},[57,328,329],{"class":101},"depmod",[57,331,332],{"class":70}," -a\n",[57,334,335,337,340,343],{"class":59,"line":190},[57,336,71],{"class":70},[57,338,339],{"class":74}," \"xt_RTPENGINE\"",[57,341,342],{"class":84}," >>",[57,344,345],{"class":74}," \u002Fetc\u002Fmodules\n",[57,347,348],{"class":59,"line":195},[57,349,95],{"emptyLinePlaceholder":94},[57,351,352],{"class":59,"line":207},[57,353,354],{"class":63},"# Verify it loaded\n",[57,356,357,360,363,366],{"class":59,"line":216},[57,358,359],{"class":101},"lsmod",[57,361,362],{"class":84}," |",[57,364,365],{"class":101}," grep",[57,367,368],{"class":74}," xt_RTPENGINE\n",[57,370,371],{"class":59,"line":222},[57,372,373],{"class":63},"# Expected: xt_RTPENGINE    16384  0\n",[16,375,376],{},"Verify kernel forwarding is active after rtpengine starts:",[48,378,380],{"className":50,"code":379,"language":52,"meta":53,"style":53},"cat \u002Fproc\u002Frtpengine\u002F0\u002Flist\n# Should show active kernel-forwarded sessions\n# Empty = kernel module not loaded, falling back to userspace\n",[28,381,382,390,395],{"__ignoreMap":53},[57,383,384,387],{"class":59,"line":60},[57,385,386],{"class":101},"cat",[57,388,389],{"class":74}," \u002Fproc\u002Frtpengine\u002F0\u002Flist\n",[57,391,392],{"class":59,"line":67},[57,393,394],{"class":63},"# Should show active kernel-forwarded sessions\n",[57,396,397],{"class":59,"line":81},[57,398,399],{"class":63},"# Empty = kernel module not loaded, falling back to userspace\n",[20,401,403],{"id":402},"configuration-file","Configuration File",[48,405,409],{"className":406,"code":407,"language":408,"meta":53,"style":53},"language-ini shiki shiki-themes github-light github-dark","# \u002Fetc\u002Frtpengine\u002Frtpengine.conf\n[rtpengine]\n# Network\ninterface = eth0\u002F203.0.113.10\nlisten-ng = 127.0.0.1:2223\nlisten-tcp-ng = 127.0.0.1:2223\n\n# Port range for RTP relay\nport-min = 30000\nport-max = 40000\n\n# DTLS\u002FSRTP\ndtls-passive = yes\ntls-certificate = \u002Fetc\u002Fssl\u002Fcerts\u002Frtpengine.pem\ntls-private-key = \u002Fetc\u002Fssl\u002Fprivate\u002Frtpengine.key\n\n# Kernel forwarding (0 = use kernel module if available)\ntable = 0\n\n# Logging\nlog-level = 5\nlog-facility = daemon\nlog-facility-rtcp = local0\n\n# Performance\ntimeout = 60\nsilent-timeout = 3600\ntos = 184                    # DSCP EF (0xB8) for RTP traffic\n\n# Transcoding (requires ffmpeg libraries)\n# codec-except = PCMU        # Uncomment to disable specific codecs\n\n# Prometheus\nprometheus = yes\nprometheus-listen = 127.0.0.1:9900\n\n# Homer SIPcapture\nhomer-ng = udp:127.0.0.1:9060\nhomer-protocol = 17\nhomer-id = 2002\n","ini",[28,410,411,416,421,426,431,436,441,445,450,455,460,464,469,474,479,484,488,494,500,505,511,517,523,529,534,540,546,552,558,563,569,575,580,586,592,598,603,609,615,621],{"__ignoreMap":53},[57,412,413],{"class":59,"line":60},[57,414,415],{},"# \u002Fetc\u002Frtpengine\u002Frtpengine.conf\n",[57,417,418],{"class":59,"line":67},[57,419,420],{},"[rtpengine]\n",[57,422,423],{"class":59,"line":81},[57,424,425],{},"# Network\n",[57,427,428],{"class":59,"line":91},[57,429,430],{},"interface = eth0\u002F203.0.113.10\n",[57,432,433],{"class":59,"line":98},[57,434,435],{},"listen-ng = 127.0.0.1:2223\n",[57,437,438],{"class":59,"line":123},[57,439,440],{},"listen-tcp-ng = 127.0.0.1:2223\n",[57,442,443],{"class":59,"line":132},[57,444,95],{"emptyLinePlaceholder":94},[57,446,447],{"class":59,"line":143},[57,448,449],{},"# Port range for RTP relay\n",[57,451,452],{"class":59,"line":148},[57,453,454],{},"port-min = 30000\n",[57,456,457],{"class":59,"line":154},[57,458,459],{},"port-max = 40000\n",[57,461,462],{"class":59,"line":175},[57,463,95],{"emptyLinePlaceholder":94},[57,465,466],{"class":59,"line":190},[57,467,468],{},"# DTLS\u002FSRTP\n",[57,470,471],{"class":59,"line":195},[57,472,473],{},"dtls-passive = yes\n",[57,475,476],{"class":59,"line":207},[57,477,478],{},"tls-certificate = \u002Fetc\u002Fssl\u002Fcerts\u002Frtpengine.pem\n",[57,480,481],{"class":59,"line":216},[57,482,483],{},"tls-private-key = \u002Fetc\u002Fssl\u002Fprivate\u002Frtpengine.key\n",[57,485,486],{"class":59,"line":222},[57,487,95],{"emptyLinePlaceholder":94},[57,489,491],{"class":59,"line":490},17,[57,492,493],{},"# Kernel forwarding (0 = use kernel module if available)\n",[57,495,497],{"class":59,"line":496},18,[57,498,499],{},"table = 0\n",[57,501,503],{"class":59,"line":502},19,[57,504,95],{"emptyLinePlaceholder":94},[57,506,508],{"class":59,"line":507},20,[57,509,510],{},"# Logging\n",[57,512,514],{"class":59,"line":513},21,[57,515,516],{},"log-level = 5\n",[57,518,520],{"class":59,"line":519},22,[57,521,522],{},"log-facility = daemon\n",[57,524,526],{"class":59,"line":525},23,[57,527,528],{},"log-facility-rtcp = local0\n",[57,530,532],{"class":59,"line":531},24,[57,533,95],{"emptyLinePlaceholder":94},[57,535,537],{"class":59,"line":536},25,[57,538,539],{},"# Performance\n",[57,541,543],{"class":59,"line":542},26,[57,544,545],{},"timeout = 60\n",[57,547,549],{"class":59,"line":548},27,[57,550,551],{},"silent-timeout = 3600\n",[57,553,555],{"class":59,"line":554},28,[57,556,557],{},"tos = 184                    # DSCP EF (0xB8) for RTP traffic\n",[57,559,561],{"class":59,"line":560},29,[57,562,95],{"emptyLinePlaceholder":94},[57,564,566],{"class":59,"line":565},30,[57,567,568],{},"# Transcoding (requires ffmpeg libraries)\n",[57,570,572],{"class":59,"line":571},31,[57,573,574],{},"# codec-except = PCMU        # Uncomment to disable specific codecs\n",[57,576,578],{"class":59,"line":577},32,[57,579,95],{"emptyLinePlaceholder":94},[57,581,583],{"class":59,"line":582},33,[57,584,585],{},"# Prometheus\n",[57,587,589],{"class":59,"line":588},34,[57,590,591],{},"prometheus = yes\n",[57,593,595],{"class":59,"line":594},35,[57,596,597],{},"prometheus-listen = 127.0.0.1:9900\n",[57,599,601],{"class":59,"line":600},36,[57,602,95],{"emptyLinePlaceholder":94},[57,604,606],{"class":59,"line":605},37,[57,607,608],{},"# Homer SIPcapture\n",[57,610,612],{"class":59,"line":611},38,[57,613,614],{},"homer-ng = udp:127.0.0.1:9060\n",[57,616,618],{"class":59,"line":617},39,[57,619,620],{},"homer-protocol = 17\n",[57,622,624],{"class":59,"line":623},40,[57,625,626],{},"homer-id = 2002\n",[16,628,629,630,633,634,637],{},"The ",[28,631,632],{},"interface"," parameter format ",[28,635,636],{},"physical-interface\u002Fpublic-ip"," is critical for NAT traversal. rtpengine binds on the physical interface IP but advertises the public IP in SDP rewriting. Get this wrong and media flows to unreachable addresses.",[20,639,641],{"id":640},"kamailio-integration","Kamailio Integration",[16,643,644,645,648],{},"Kamailio controls rtpengine via the ",[28,646,647],{},"rtpengine"," module using the ng control protocol:",[48,650,655],{"className":651,"code":653,"language":654},[652],"language-text","loadmodule \"rtpengine.so\"\n\nmodparam(\"rtpengine\", \"rtpengine_sock\", \"udp:127.0.0.1:2223\")\nmodparam(\"rtpengine\", \"rtpengine_disable_tout\", 20)\nmodparam(\"rtpengine\", \"rtpengine_retr\", 5)\nmodparam(\"rtpengine\", \"rtpengine_tout_ms\", 1000)\n\n# For multiple rtpengine instances:\n# modparam(\"rtpengine\", \"rtpengine_sock\", \"udp:10.0.1.10:2223 udp:10.0.1.11:2223\")\n\nrequest_route {\n    if (is_method(\"INVITE\")) {\n        if (has_body(\"application\u002Fsdp\")) {\n            # Offer — rewrite SDP to proxy through rtpengine\n            rtpengine_offer(\"trust-address replace-origin replace-session-connection\");\n        }\n    }\n    \n    if (is_method(\"BYE\") || is_method(\"CANCEL\")) {\n        rtpengine_delete();\n    }\n    \n    t_relay();\n}\n\nonreply_route {\n    if (t_check_status(\"18[0-9]\") || t_check_status(\"2[0-9][0-9]\")) {\n        if (has_body(\"application\u002Fsdp\")) {\n            # Answer — complete the SDP rewrite\n            rtpengine_answer(\"trust-address replace-origin replace-session-connection\");\n        }\n    }\n}\n","text",[28,656,653],{"__ignoreMap":53},[16,658,659],{},"The flags string controls rtpengine behavior:",[661,662,663,676],"table",{},[664,665,666],"thead",{},[667,668,669,673],"tr",{},[670,671,672],"th",{},"Flag",[670,674,675],{},"Effect",[677,678,679,690,704,718,728,738,748,758],"tbody",{},[667,680,681,687],{},[682,683,684],"td",{},[28,685,686],{},"trust-address",[682,688,689],{},"Trust the SDP connection address (don't override with signaling source IP)",[667,691,692,697],{},[682,693,694],{},[28,695,696],{},"replace-origin",[682,698,699,700,703],{},"Rewrite the SDP ",[28,701,702],{},"o="," line with rtpengine's address",[667,705,706,711],{},[682,707,708],{},[28,709,710],{},"replace-session-connection",[682,712,713,714,717],{},"Rewrite the session-level ",[28,715,716],{},"c="," line",[667,719,720,725],{},[682,721,722],{},[28,723,724],{},"ICE=remove",[682,726,727],{},"Strip ICE candidates (for SIP-to-SIP, not WebRTC)",[667,729,730,735],{},[682,731,732],{},[28,733,734],{},"DTLS=passive",[682,736,737],{},"Force DTLS passive mode (for WebRTC endpoints)",[667,739,740,745],{},[682,741,742],{},[28,743,744],{},"SRTP",[682,746,747],{},"Enable SRTP for this leg",[667,749,750,755],{},[682,751,752],{},[28,753,754],{},"transcode-PCMU",[682,756,757],{},"Transcode to PCMU if needed",[667,759,760,765],{},[682,761,762],{},[28,763,764],{},"record-call=yes",[682,766,767],{},"Enable call recording for this session",[20,769,771],{"id":770},"srtp-transcoding-plain-sip-to-webrtc","SRTP Transcoding: Plain SIP to WebRTC",[16,773,774],{},"The most common rtpengine use case is bridging plain SIP (with plain RTP) to WebRTC (which requires SRTP\u002FDTLS). rtpengine handles the SRTP key exchange and media encryption transparently:",[48,776,779],{"className":777,"code":778,"language":654},[652],"# For the WebRTC leg (DTLS-SRTP required)\nrtpengine_offer(\"ICE=force DTLS=passive SDES-off\");\n\n# For the SIP leg (plain RTP)\nrtpengine_answer(\"ICE=remove DTLS=off SDES-off\");\n",[28,780,778],{"__ignoreMap":53},[16,782,783],{},"With this configuration, rtpengine:",[785,786,787,791,794,797],"ol",{},[788,789,790],"li",{},"Receives DTLS from the WebRTC client and negotiates SRTP keys",[788,792,793],{},"Decrypts SRTP from the WebRTC client",[788,795,796],{},"Forwards plain RTP to the SIP endpoint",[788,798,799],{},"Encrypts in the reverse direction",[16,801,802,803,805],{},"This all happens in userspace (DTLS negotiation cannot be kernel-forwarded), but once the session is established and ",[28,804,30],{}," takes over, subsequent packets bypass userspace.",[20,807,809],{"id":808},"multi-instance-clustering","Multi-Instance Clustering",[16,811,812],{},"For high-availability and horizontal scaling, run multiple rtpengine instances and let Kamailio distribute sessions:",[48,814,817],{"className":815,"code":816,"language":654},[652],"modparam(\"rtpengine\", \"rtpengine_sock\",\n    \"udp:10.0.1.10:2223 udp:10.0.1.11:2223 udp:10.0.1.12:2223\")\n\nmodparam(\"rtpengine\", \"rtpengine_disable_tout\", 20)\n",[28,818,816],{"__ignoreMap":53},[16,820,821,822,825],{},"Kamailio's rtpengine module does round-robin across healthy instances. If an instance stops responding, Kamailio marks it disabled and routes to the remaining instances after ",[28,823,824],{},"rtpengine_disable_tout"," seconds.",[16,827,828,829,832],{},"In-dialog requests (re-INVITE, UPDATE) must reach the same rtpengine instance that handled the original INVITE. Kamailio tracks this via the ",[28,830,831],{},"rtpengine_manage()"," function which reads the stored instance from the dialog:",[48,834,837],{"className":835,"code":836,"language":654},[652],"# Use rtpengine_manage() for in-dialog requests\n# It automatically selects the correct instance\nif (has_totag()) {\n    if (is_method(\"INVITE|UPDATE|ACK\")) {\n        rtpengine_manage(\"trust-address replace-origin\");\n    }\n}\n",[28,838,836],{"__ignoreMap":53},[20,840,842],{"id":841},"capacity-planning","Capacity Planning",[661,844,845,858],{},[664,846,847],{},[667,848,849,852,855],{},[670,850,851],{},"Configuration",[670,853,854],{},"Concurrent sessions",[670,856,857],{},"Packet rate",[677,859,860,871,882,893],{},[667,861,862,865,868],{},[682,863,864],{},"Userspace only, 1 core",[682,866,867],{},"~5,000",[682,869,870],{},"~300K pps",[667,872,873,876,879],{},[682,874,875],{},"Kernel module, 1 core",[682,877,878],{},"~15,000",[682,880,881],{},"~1M pps",[667,883,884,887,890],{},[682,885,886],{},"Kernel module, 4 cores",[682,888,889],{},"~50,000",[682,891,892],{},"~3M pps",[667,894,895,898,901],{},[682,896,897],{},"Kernel module, 16 cores",[682,899,900],{},"~100,000+",[682,902,903],{},"~8M pps",[16,905,906],{},"Memory is minimal: ~1 KB per session for the kernel forwarding table, ~50 KB per session for the userspace session record. A server with 16 GB RAM handles 100,000 concurrent sessions with memory to spare.",[16,908,909],{},"Bandwidth is the real constraint. Each audio session consumes ~100 Kbps bidirectional (G.711 \u002F PCMU). 10,000 sessions = 1 Gbps throughput. Size your NIC and carrier bandwidth accordingly:",[48,911,913],{"className":50,"code":912,"language":52,"meta":53,"style":53},"# Check current throughput\ncat \u002Fproc\u002Frtpengine\u002F0\u002Fstats | grep bytes_forwarded\n# Or via Prometheus: rtpengine_total_traffic_bytes\n\n# Check kernel forwarding table size\ncat \u002Fproc\u002Frtpengine\u002F0\u002Flist | wc -l\n",[28,914,915,920,934,939,943,948],{"__ignoreMap":53},[57,916,917],{"class":59,"line":60},[57,918,919],{"class":63},"# Check current throughput\n",[57,921,922,924,927,929,931],{"class":59,"line":67},[57,923,386],{"class":101},[57,925,926],{"class":74}," \u002Fproc\u002Frtpengine\u002F0\u002Fstats",[57,928,362],{"class":84},[57,930,365],{"class":101},[57,932,933],{"class":74}," bytes_forwarded\n",[57,935,936],{"class":59,"line":81},[57,937,938],{"class":63},"# Or via Prometheus: rtpengine_total_traffic_bytes\n",[57,940,941],{"class":59,"line":91},[57,942,95],{"emptyLinePlaceholder":94},[57,944,945],{"class":59,"line":98},[57,946,947],{"class":63},"# Check kernel forwarding table size\n",[57,949,950,952,955,957,960],{"class":59,"line":123},[57,951,386],{"class":101},[57,953,954],{"class":74}," \u002Fproc\u002Frtpengine\u002F0\u002Flist",[57,956,362],{"class":84},[57,958,959],{"class":101}," wc",[57,961,962],{"class":70}," -l\n",[20,964,966],{"id":965},"call-recording-via-rtpengine","Call Recording via rtpengine",[16,968,969,970,973],{},"rtpengine supports inline call recording via the ",[28,971,972],{},"record-call"," flag. Recordings go to a local directory as PCAP files:",[48,975,977],{"className":406,"code":976,"language":408,"meta":53,"style":53},"# rtpengine.conf\nrecording-dir = \u002Fvar\u002Fspool\u002Frtpengine-recordings\nrecording-method = pcap\nrecording-format = eth\n",[28,978,979,984,989,994],{"__ignoreMap":53},[57,980,981],{"class":59,"line":60},[57,982,983],{},"# rtpengine.conf\n",[57,985,986],{"class":59,"line":67},[57,987,988],{},"recording-dir = \u002Fvar\u002Fspool\u002Frtpengine-recordings\n",[57,990,991],{"class":59,"line":81},[57,992,993],{},"recording-method = pcap\n",[57,995,996],{"class":59,"line":91},[57,997,998],{},"recording-format = eth\n",[48,1000,1003],{"className":1001,"code":1002,"language":654},[652],"# Kamailio: enable recording for specific calls\nif ($avp(record_call) == \"yes\") {\n    rtpengine_offer(\"trust-address replace-origin record-call=yes\");\n} else {\n    rtpengine_offer(\"trust-address replace-origin record-call=no\");\n}\n",[28,1004,1002],{"__ignoreMap":53},[16,1006,1007],{},"PCAP recordings include all RTP\u002FRTCP packets. Import them into Wireshark with Telephony > VoIP Calls for playback and quality analysis. At scale, stream recordings directly to S3 using rtpengine's S3 output mode (available in enterprise builds) rather than accumulating PCAP files on the local filesystem.",[1009,1010,1011],"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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":53,"searchDepth":67,"depth":67,"links":1013},[1014,1015,1016,1017,1018,1019,1020,1021,1022],{"id":22,"depth":67,"text":23},{"id":45,"depth":67,"text":46},{"id":231,"depth":67,"text":232},{"id":402,"depth":67,"text":403},{"id":640,"depth":67,"text":641},{"id":770,"depth":67,"text":771},{"id":808,"depth":67,"text":809},{"id":841,"depth":67,"text":842},{"id":965,"depth":67,"text":966},"SBC","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1200&q=80","2026-03-01","Deploy rtpengine as a high-performance RTP proxy: kernel forwarding module, Kamailio integration, SRTP transcoding, multi-instance clustering, and capacity planning.","md",{},"\u002Fblog\u002Frtpengine-deployment-guide",{"title":6,"description":1026},"blog\u002Frtpengine-deployment-guide",[647,1033,1034,1035,1036,1037,1038],"rtp-proxy","sbc","srtp","kamailio","media-proxy","nat-traversal","wHYzAzP3MgaoxMqqw7j2Lyp433riL0OWI-4nsrc710Q",{"id":1041,"title":1042,"author":7,"body":1043,"category":2694,"coverImage":2695,"date":2696,"description":2697,"extension":1027,"meta":2698,"navigation":94,"path":2699,"readingTime":154,"seo":2700,"stem":2701,"tags":2702,"__hash__":2709},"posts\u002Fblog\u002Fvoip-monitoring-prometheus-grafana.md","VoIP Monitoring with Prometheus and Grafana",{"type":9,"value":1044,"toc":2681},[1045,1048,1051,1055,1058,1119,1122,1126,1137,1140,1223,1226,1232,1235,1377,1381,1384,1389,1492,1527,1531,1534,1707,1711,1718,1739,1742,1824,1827,1831,2301,2305,2308,2314,2329,2334,2348,2353,2364,2369,2383,2387,2645,2649,2652,2675,2678],[12,1046,1042],{"id":1047},"voip-monitoring-with-prometheus-and-grafana",[16,1049,1050],{},"VoIP systems fail in ways that don't show up in generic infrastructure monitoring. CPU at 20%, memory healthy, network up — but calls are dropping because the SIP registrar is rejecting REGISTERs due to a certificate expiry, or RTP packet loss is hitting 8% on a specific carrier route, or the T38 fax relay is silently failing. This post covers building a VoIP-specific observability stack that surfaces the metrics that matter: call quality, registration health, SIP error rates, and carrier route performance.",[20,1052,1054],{"id":1053},"what-to-monitor-in-voip-infrastructure","What to Monitor in VoIP Infrastructure",[16,1056,1057],{},"Before instrumenting anything, define the four signal types for VoIP:",[661,1059,1060,1073],{},[664,1061,1062],{},[667,1063,1064,1067,1070],{},[670,1065,1066],{},"Signal",[670,1068,1069],{},"Examples",[670,1071,1072],{},"Tool",[677,1074,1075,1086,1097,1108],{},[667,1076,1077,1080,1083],{},[682,1078,1079],{},"SIP signaling",[682,1081,1082],{},"INVITE rate, 4xx\u002F5xx rate, REGISTER failures",[682,1084,1085],{},"kamailio_exporter, snmp_exporter",[667,1087,1088,1091,1094],{},[682,1089,1090],{},"Media quality",[682,1092,1093],{},"Packet loss, jitter, MOS score",[682,1095,1096],{},"rtpengine metrics, Homer SIPcapture",[667,1098,1099,1102,1105],{},[682,1100,1101],{},"Infrastructure",[682,1103,1104],{},"CPU, memory, network I\u002FO, disk",[682,1106,1107],{},"node_exporter",[667,1109,1110,1113,1116],{},[682,1111,1112],{},"Business",[682,1114,1115],{},"ASR (answer rate), ACD (avg call duration), NER",[682,1117,1118],{},"CDR database queries",[16,1120,1121],{},"A complete monitoring setup scrapes all four. Infrastructure metrics alone give you uptime; the other three give you quality.",[20,1123,1125],{"id":1124},"kamailio-metrics-with-kamailio_exporter","Kamailio Metrics with kamailio_exporter",[16,1127,1128,1129,1132,1133,1136],{},"Kamailio exposes internal statistics via the ",[28,1130,1131],{},"statistics"," module. The ",[28,1134,1135],{},"kamailio_exporter"," translates these to Prometheus metrics.",[16,1138,1139],{},"Install the exporter:",[48,1141,1143],{"className":50,"code":1142,"language":52,"meta":53,"style":53},"# Run kamailio_exporter as a sidecar\ndocker run -d \\\n  --name kamailio-exporter \\\n  --network host \\\n  -e KAMAILIO_HOST=127.0.0.1 \\\n  -e KAMAILIO_PORT=5060 \\\n  -p 9494:9494 \\\n  hunterlong\u002Fkamailio-exporter\n",[28,1144,1145,1150,1163,1173,1183,1196,1208,1218],{"__ignoreMap":53},[57,1146,1147],{"class":59,"line":60},[57,1148,1149],{"class":63},"# Run kamailio_exporter as a sidecar\n",[57,1151,1152,1155,1158,1161],{"class":59,"line":67},[57,1153,1154],{"class":101},"docker",[57,1156,1157],{"class":74}," run",[57,1159,1160],{"class":70}," -d",[57,1162,78],{"class":70},[57,1164,1165,1168,1171],{"class":59,"line":81},[57,1166,1167],{"class":70},"  --name",[57,1169,1170],{"class":74}," kamailio-exporter",[57,1172,78],{"class":70},[57,1174,1175,1178,1181],{"class":59,"line":91},[57,1176,1177],{"class":70},"  --network",[57,1179,1180],{"class":74}," host",[57,1182,78],{"class":70},[57,1184,1185,1188,1191,1194],{"class":59,"line":98},[57,1186,1187],{"class":70},"  -e",[57,1189,1190],{"class":74}," KAMAILIO_HOST=",[57,1192,1193],{"class":70},"127.0.0.1",[57,1195,78],{"class":70},[57,1197,1198,1200,1203,1206],{"class":59,"line":123},[57,1199,1187],{"class":70},[57,1201,1202],{"class":74}," KAMAILIO_PORT=",[57,1204,1205],{"class":70},"5060",[57,1207,78],{"class":70},[57,1209,1210,1213,1216],{"class":59,"line":132},[57,1211,1212],{"class":70},"  -p",[57,1214,1215],{"class":74}," 9494:9494",[57,1217,78],{"class":70},[57,1219,1220],{"class":59,"line":143},[57,1221,1222],{"class":74},"  hunterlong\u002Fkamailio-exporter\n",[16,1224,1225],{},"Or configure Kamailio to expose a JSON stats endpoint:",[48,1227,1230],{"className":1228,"code":1229,"language":654},[652],"loadmodule \"xhttp.so\"\nloadmodule \"statistics.so\"\n\nevent_route[xhttp:request] {\n    if ($hu =~ \"^\u002Fmetrics\") {\n        xhttp_reply(\"200\", \"OK\", \"text\u002Fplain\", $stat(all));\n        exit;\n    }\n}\n",[28,1231,1229],{"__ignoreMap":53},[16,1233,1234],{},"Key Kamailio metrics to track:",[48,1236,1240],{"className":1237,"code":1238,"language":1239,"meta":53,"style":53},"language-yaml shiki shiki-themes github-light github-dark","# prometheus\u002Frecording_rules.yml\ngroups:\n  - name: kamailio_derived\n    rules:\n      - record: kamailio:sip_4xx_rate\n        expr: rate(kamailio_core_rcv_requests_total{method=\"INVITE\",status=~\"4..\"}[5m])\n\n      - record: kamailio:register_failure_rate\n        expr: rate(kamailio_core_rcv_requests_total{method=\"REGISTER\",status=\"401\"}[5m])\n           \u002F rate(kamailio_core_rcv_requests_total{method=\"REGISTER\"}[5m])\n\n      - record: kamailio:active_dialogs\n        expr: kamailio_dialog_active\n\n      - record: kamailio:invite_per_second\n        expr: rate(kamailio_core_rcv_requests_total{method=\"INVITE\"}[1m])\n","yaml",[28,1241,1242,1247,1256,1270,1277,1290,1300,1304,1315,1324,1329,1333,1344,1353,1357,1368],{"__ignoreMap":53},[57,1243,1244],{"class":59,"line":60},[57,1245,1246],{"class":63},"# prometheus\u002Frecording_rules.yml\n",[57,1248,1249,1253],{"class":59,"line":67},[57,1250,1252],{"class":1251},"s9eBZ","groups",[57,1254,1255],{"class":254},":\n",[57,1257,1258,1261,1264,1267],{"class":59,"line":81},[57,1259,1260],{"class":254},"  - ",[57,1262,1263],{"class":1251},"name",[57,1265,1266],{"class":254},": ",[57,1268,1269],{"class":74},"kamailio_derived\n",[57,1271,1272,1275],{"class":59,"line":91},[57,1273,1274],{"class":1251},"    rules",[57,1276,1255],{"class":254},[57,1278,1279,1282,1285,1287],{"class":59,"line":98},[57,1280,1281],{"class":254},"      - ",[57,1283,1284],{"class":1251},"record",[57,1286,1266],{"class":254},[57,1288,1289],{"class":74},"kamailio:sip_4xx_rate\n",[57,1291,1292,1295,1297],{"class":59,"line":123},[57,1293,1294],{"class":1251},"        expr",[57,1296,1266],{"class":254},[57,1298,1299],{"class":74},"rate(kamailio_core_rcv_requests_total{method=\"INVITE\",status=~\"4..\"}[5m])\n",[57,1301,1302],{"class":59,"line":132},[57,1303,95],{"emptyLinePlaceholder":94},[57,1305,1306,1308,1310,1312],{"class":59,"line":143},[57,1307,1281],{"class":254},[57,1309,1284],{"class":1251},[57,1311,1266],{"class":254},[57,1313,1314],{"class":74},"kamailio:register_failure_rate\n",[57,1316,1317,1319,1321],{"class":59,"line":148},[57,1318,1294],{"class":1251},[57,1320,1266],{"class":254},[57,1322,1323],{"class":74},"rate(kamailio_core_rcv_requests_total{method=\"REGISTER\",status=\"401\"}[5m])\n",[57,1325,1326],{"class":59,"line":154},[57,1327,1328],{"class":74},"           \u002F rate(kamailio_core_rcv_requests_total{method=\"REGISTER\"}[5m])\n",[57,1330,1331],{"class":59,"line":175},[57,1332,95],{"emptyLinePlaceholder":94},[57,1334,1335,1337,1339,1341],{"class":59,"line":190},[57,1336,1281],{"class":254},[57,1338,1284],{"class":1251},[57,1340,1266],{"class":254},[57,1342,1343],{"class":74},"kamailio:active_dialogs\n",[57,1345,1346,1348,1350],{"class":59,"line":195},[57,1347,1294],{"class":1251},[57,1349,1266],{"class":254},[57,1351,1352],{"class":74},"kamailio_dialog_active\n",[57,1354,1355],{"class":59,"line":207},[57,1356,95],{"emptyLinePlaceholder":94},[57,1358,1359,1361,1363,1365],{"class":59,"line":216},[57,1360,1281],{"class":254},[57,1362,1284],{"class":1251},[57,1364,1266],{"class":254},[57,1366,1367],{"class":74},"kamailio:invite_per_second\n",[57,1369,1370,1372,1374],{"class":59,"line":222},[57,1371,1294],{"class":1251},[57,1373,1266],{"class":254},[57,1375,1376],{"class":74},"rate(kamailio_core_rcv_requests_total{method=\"INVITE\"}[1m])\n",[20,1378,1380],{"id":1379},"asterisk-metrics","Asterisk Metrics",[16,1382,1383],{},"Asterisk does not natively expose Prometheus metrics. Use one of two approaches:",[1385,1386,1388],"h3",{"id":1387},"option-1-asterisk_exporter-ami-based","Option 1: asterisk_exporter (AMI-based)",[48,1390,1392],{"className":1237,"code":1391,"language":1239,"meta":53,"style":53},"# \u002Fetc\u002Fasterisk_exporter\u002Fconfig.yml\nami:\n  host: 127.0.0.1\n  port: 5038\n  username: prometheus\n  password: secret\n\nmetrics:\n  - active_channels\n  - active_calls\n  - active_agents\n  - queue_waiting\n  - queue_completed\n",[28,1393,1394,1399,1406,1416,1426,1436,1446,1450,1457,1464,1471,1478,1485],{"__ignoreMap":53},[57,1395,1396],{"class":59,"line":60},[57,1397,1398],{"class":63},"# \u002Fetc\u002Fasterisk_exporter\u002Fconfig.yml\n",[57,1400,1401,1404],{"class":59,"line":67},[57,1402,1403],{"class":1251},"ami",[57,1405,1255],{"class":254},[57,1407,1408,1411,1413],{"class":59,"line":81},[57,1409,1410],{"class":1251},"  host",[57,1412,1266],{"class":254},[57,1414,1415],{"class":70},"127.0.0.1\n",[57,1417,1418,1421,1423],{"class":59,"line":91},[57,1419,1420],{"class":1251},"  port",[57,1422,1266],{"class":254},[57,1424,1425],{"class":70},"5038\n",[57,1427,1428,1431,1433],{"class":59,"line":98},[57,1429,1430],{"class":1251},"  username",[57,1432,1266],{"class":254},[57,1434,1435],{"class":74},"prometheus\n",[57,1437,1438,1441,1443],{"class":59,"line":123},[57,1439,1440],{"class":1251},"  password",[57,1442,1266],{"class":254},[57,1444,1445],{"class":74},"secret\n",[57,1447,1448],{"class":59,"line":132},[57,1449,95],{"emptyLinePlaceholder":94},[57,1451,1452,1455],{"class":59,"line":143},[57,1453,1454],{"class":1251},"metrics",[57,1456,1255],{"class":254},[57,1458,1459,1461],{"class":59,"line":148},[57,1460,1260],{"class":254},[57,1462,1463],{"class":74},"active_channels\n",[57,1465,1466,1468],{"class":59,"line":154},[57,1467,1260],{"class":254},[57,1469,1470],{"class":74},"active_calls\n",[57,1472,1473,1475],{"class":59,"line":175},[57,1474,1260],{"class":254},[57,1476,1477],{"class":74},"active_agents\n",[57,1479,1480,1482],{"class":59,"line":190},[57,1481,1260],{"class":254},[57,1483,1484],{"class":74},"queue_waiting\n",[57,1486,1487,1489],{"class":59,"line":195},[57,1488,1260],{"class":254},[57,1490,1491],{"class":74},"queue_completed\n",[48,1493,1495],{"className":406,"code":1494,"language":408,"meta":53,"style":53},"# \u002Fetc\u002Fasterisk\u002Fmanager.conf\n[prometheus]\nsecret=secret\npermit=127.0.0.1\u002F255.255.255.255\nread=system,call,agent,user,config,dtmf,reporting,cdr,dialplan\nwrite=\n",[28,1496,1497,1502,1507,1512,1517,1522],{"__ignoreMap":53},[57,1498,1499],{"class":59,"line":60},[57,1500,1501],{},"# \u002Fetc\u002Fasterisk\u002Fmanager.conf\n",[57,1503,1504],{"class":59,"line":67},[57,1505,1506],{},"[prometheus]\n",[57,1508,1509],{"class":59,"line":81},[57,1510,1511],{},"secret=secret\n",[57,1513,1514],{"class":59,"line":91},[57,1515,1516],{},"permit=127.0.0.1\u002F255.255.255.255\n",[57,1518,1519],{"class":59,"line":98},[57,1520,1521],{},"read=system,call,agent,user,config,dtmf,reporting,cdr,dialplan\n",[57,1523,1524],{"class":59,"line":123},[57,1525,1526],{},"write=\n",[1385,1528,1530],{"id":1529},"option-2-cel-to-prometheus-via-lokigrafana-pipeline","Option 2: CEL to Prometheus via Loki\u002FGrafana pipeline",[16,1532,1533],{},"Write CDR\u002FCEL events to a PostgreSQL table and expose them via a custom exporter. This approach gives you business metrics (ASR, ACD, call volumes by trunk) that the AMI exporter cannot provide:",[48,1535,1539],{"className":1536,"code":1537,"language":1538,"meta":53,"style":53},"language-python shiki shiki-themes github-light github-dark","# asterisk_business_exporter.py\nfrom prometheus_client import Gauge, start_http_server\nimport psycopg2\nimport time\n\nasr_gauge = Gauge('asterisk_asr_ratio', 'Answer-Seizure Ratio', ['trunk'])\nacd_gauge = Gauge('asterisk_acd_seconds', 'Average Call Duration', ['trunk'])\ncalls_gauge = Gauge('asterisk_active_calls', 'Active calls', ['direction'])\n\ndef collect_metrics():\n    conn = psycopg2.connect(\"host=localhost dbname=asterisk_cdr user=monitor\")\n    cur = conn.cursor()\n    \n    # ASR per trunk (last 5 minutes)\n    cur.execute(\"\"\"\n        SELECT\n            accountcode AS trunk,\n            ROUND(AVG(CASE WHEN disposition='ANSWERED' THEN 1.0 ELSE 0.0 END), 3) AS asr,\n            AVG(CASE WHEN disposition='ANSWERED' THEN billsec ELSE NULL END) AS acd\n        FROM cdr\n        WHERE calldate > NOW() - INTERVAL '5 minutes'\n          AND accountcode IS NOT NULL\n        GROUP BY accountcode\n    \"\"\")\n    \n    for trunk, asr, acd in cur.fetchall():\n        asr_gauge.labels(trunk=trunk).set(asr or 0)\n        acd_gauge.labels(trunk=trunk).set(acd or 0)\n\nif __name__ == '__main__':\n    start_http_server(9200)\n    while True:\n        collect_metrics()\n        time.sleep(30)\n","python",[28,1540,1541,1546,1551,1556,1561,1565,1570,1575,1580,1584,1589,1594,1599,1604,1609,1614,1619,1624,1629,1634,1639,1644,1649,1654,1659,1663,1668,1673,1678,1682,1687,1692,1697,1702],{"__ignoreMap":53},[57,1542,1543],{"class":59,"line":60},[57,1544,1545],{},"# asterisk_business_exporter.py\n",[57,1547,1548],{"class":59,"line":67},[57,1549,1550],{},"from prometheus_client import Gauge, start_http_server\n",[57,1552,1553],{"class":59,"line":81},[57,1554,1555],{},"import psycopg2\n",[57,1557,1558],{"class":59,"line":91},[57,1559,1560],{},"import time\n",[57,1562,1563],{"class":59,"line":98},[57,1564,95],{"emptyLinePlaceholder":94},[57,1566,1567],{"class":59,"line":123},[57,1568,1569],{},"asr_gauge = Gauge('asterisk_asr_ratio', 'Answer-Seizure Ratio', ['trunk'])\n",[57,1571,1572],{"class":59,"line":132},[57,1573,1574],{},"acd_gauge = Gauge('asterisk_acd_seconds', 'Average Call Duration', ['trunk'])\n",[57,1576,1577],{"class":59,"line":143},[57,1578,1579],{},"calls_gauge = Gauge('asterisk_active_calls', 'Active calls', ['direction'])\n",[57,1581,1582],{"class":59,"line":148},[57,1583,95],{"emptyLinePlaceholder":94},[57,1585,1586],{"class":59,"line":154},[57,1587,1588],{},"def collect_metrics():\n",[57,1590,1591],{"class":59,"line":175},[57,1592,1593],{},"    conn = psycopg2.connect(\"host=localhost dbname=asterisk_cdr user=monitor\")\n",[57,1595,1596],{"class":59,"line":190},[57,1597,1598],{},"    cur = conn.cursor()\n",[57,1600,1601],{"class":59,"line":195},[57,1602,1603],{},"    \n",[57,1605,1606],{"class":59,"line":207},[57,1607,1608],{},"    # ASR per trunk (last 5 minutes)\n",[57,1610,1611],{"class":59,"line":216},[57,1612,1613],{},"    cur.execute(\"\"\"\n",[57,1615,1616],{"class":59,"line":222},[57,1617,1618],{},"        SELECT\n",[57,1620,1621],{"class":59,"line":490},[57,1622,1623],{},"            accountcode AS trunk,\n",[57,1625,1626],{"class":59,"line":496},[57,1627,1628],{},"            ROUND(AVG(CASE WHEN disposition='ANSWERED' THEN 1.0 ELSE 0.0 END), 3) AS asr,\n",[57,1630,1631],{"class":59,"line":502},[57,1632,1633],{},"            AVG(CASE WHEN disposition='ANSWERED' THEN billsec ELSE NULL END) AS acd\n",[57,1635,1636],{"class":59,"line":507},[57,1637,1638],{},"        FROM cdr\n",[57,1640,1641],{"class":59,"line":513},[57,1642,1643],{},"        WHERE calldate > NOW() - INTERVAL '5 minutes'\n",[57,1645,1646],{"class":59,"line":519},[57,1647,1648],{},"          AND accountcode IS NOT NULL\n",[57,1650,1651],{"class":59,"line":525},[57,1652,1653],{},"        GROUP BY accountcode\n",[57,1655,1656],{"class":59,"line":531},[57,1657,1658],{},"    \"\"\")\n",[57,1660,1661],{"class":59,"line":536},[57,1662,1603],{},[57,1664,1665],{"class":59,"line":542},[57,1666,1667],{},"    for trunk, asr, acd in cur.fetchall():\n",[57,1669,1670],{"class":59,"line":548},[57,1671,1672],{},"        asr_gauge.labels(trunk=trunk).set(asr or 0)\n",[57,1674,1675],{"class":59,"line":554},[57,1676,1677],{},"        acd_gauge.labels(trunk=trunk).set(acd or 0)\n",[57,1679,1680],{"class":59,"line":560},[57,1681,95],{"emptyLinePlaceholder":94},[57,1683,1684],{"class":59,"line":565},[57,1685,1686],{},"if __name__ == '__main__':\n",[57,1688,1689],{"class":59,"line":571},[57,1690,1691],{},"    start_http_server(9200)\n",[57,1693,1694],{"class":59,"line":577},[57,1695,1696],{},"    while True:\n",[57,1698,1699],{"class":59,"line":582},[57,1700,1701],{},"        collect_metrics()\n",[57,1703,1704],{"class":59,"line":588},[57,1705,1706],{},"        time.sleep(30)\n",[20,1708,1710],{"id":1709},"rtpengine-metrics","rtpengine Metrics",[16,1712,1713,1714,1717],{},"rtpengine exposes Prometheus metrics natively when built with ",[28,1715,1716],{},"--with-transcoding",":",[48,1719,1721],{"className":406,"code":1720,"language":408,"meta":53,"style":53},"# \u002Fetc\u002Frtpengine\u002Frtpengine.conf\n[rtpengine]\nprometheus = yes\nprometheus-listen = 127.0.0.1:9900\n",[28,1722,1723,1727,1731,1735],{"__ignoreMap":53},[57,1724,1725],{"class":59,"line":60},[57,1726,415],{},[57,1728,1729],{"class":59,"line":67},[57,1730,420],{},[57,1732,1733],{"class":59,"line":81},[57,1734,591],{},[57,1736,1737],{"class":59,"line":91},[57,1738,597],{},[16,1740,1741],{},"Key media quality metrics from rtpengine:",[661,1743,1744,1757],{},[664,1745,1746],{},[667,1747,1748,1751,1754],{},[670,1749,1750],{},"Metric",[670,1752,1753],{},"Alert threshold",[670,1755,1756],{},"Description",[677,1758,1759,1772,1785,1798,1811],{},[667,1760,1761,1766,1769],{},[682,1762,1763],{},[28,1764,1765],{},"rtpengine_packet_loss_ratio",[682,1767,1768],{},"> 0.03",[682,1770,1771],{},"Packet loss > 3%",[667,1773,1774,1779,1782],{},[682,1775,1776],{},[28,1777,1778],{},"rtpengine_jitter_ms",[682,1780,1781],{},"> 50",[682,1783,1784],{},"Jitter > 50ms",[667,1786,1787,1792,1795],{},[682,1788,1789],{},[28,1790,1791],{},"rtpengine_mos_score",[682,1793,1794],{},"\u003C 3.5",[682,1796,1797],{},"MOS below acceptable",[667,1799,1800,1805,1808],{},[682,1801,1802],{},[28,1803,1804],{},"rtpengine_active_sessions",[682,1806,1807],{},"> 80% capacity",[682,1809,1810],{},"Approaching session limit",[667,1812,1813,1818,1821],{},[682,1814,1815],{},[28,1816,1817],{},"rtpengine_transcoded_sessions",[682,1819,1820],{},"Rate spike",[682,1822,1823],{},"Unexpected transcoding",[16,1825,1826],{},"MOS (Mean Opinion Score) ranges from 1 (unusable) to 5 (excellent). A score above 4.0 is toll-quality; 3.5–4.0 is acceptable; below 3.5 users notice degradation. Set your alert at 3.5.",[20,1828,1830],{"id":1829},"prometheus-alerting-rules","Prometheus Alerting Rules",[48,1832,1834],{"className":1237,"code":1833,"language":1239,"meta":53,"style":53},"# prometheus\u002Falerts\u002Fvoip.yml\ngroups:\n  - name: voip_sip\n    rules:\n      - alert: HighSIP5xxRate\n        expr: |\n          rate(kamailio_core_rcv_replies_total{status=~\"5..\"}[5m])\n          \u002F rate(kamailio_core_rcv_replies_total[5m]) > 0.05\n        for: 3m\n        labels:\n          severity: critical\n          team: voip\n        annotations:\n          summary: \"SIP 5xx rate {{ $value | humanizePercentage }} on {{ $labels.instance }}\"\n          runbook: \"https:\u002F\u002Fwiki.example.com\u002Frunbooks\u002Fsip-5xx\"\n\n      - alert: KamailioDialogsHigh\n        expr: kamailio:active_dialogs > 8000\n        for: 5m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"Active dialogs approaching capacity: {{ $value }}\"\n\n      - alert: RegistrationFailureSpike\n        expr: kamailio:register_failure_rate > 0.2\n        for: 2m\n        labels:\n          severity: critical\n        annotations:\n          summary: \"20%+ of SIP registrations failing — possible auth issue or attack\"\n\n  - name: voip_media\n    rules:\n      - alert: MediaQualityDegraded\n        expr: rtpengine_mos_score \u003C 3.5\n        for: 5m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"MOS score {{ $value }} below 3.5 on {{ $labels.instance }}\"\n\n      - alert: MediaPacketLossHigh\n        expr: rtpengine_packet_loss_ratio > 0.03\n        for: 3m\n        labels:\n          severity: critical\n        annotations:\n          summary: \"RTP packet loss {{ $value | humanizePercentage }} — calls impacted\"\n\n      - alert: rtpengineCapacityHigh\n        expr: rtpengine_active_sessions \u002F rtpengine_max_sessions > 0.85\n        for: 5m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"rtpengine at {{ $value | humanizePercentage }} capacity\"\n",[28,1835,1836,1841,1847,1858,1864,1876,1885,1890,1895,1905,1912,1922,1932,1939,1949,1959,1963,1974,1983,1992,1998,2007,2013,2022,2026,2037,2046,2055,2061,2069,2075,2084,2088,2099,2105,2116,2125,2133,2139,2147,2153,2163,2168,2180,2190,2199,2206,2215,2222,2232,2237,2249,2259,2268,2275,2284,2291],{"__ignoreMap":53},[57,1837,1838],{"class":59,"line":60},[57,1839,1840],{"class":63},"# prometheus\u002Falerts\u002Fvoip.yml\n",[57,1842,1843,1845],{"class":59,"line":67},[57,1844,1252],{"class":1251},[57,1846,1255],{"class":254},[57,1848,1849,1851,1853,1855],{"class":59,"line":81},[57,1850,1260],{"class":254},[57,1852,1263],{"class":1251},[57,1854,1266],{"class":254},[57,1856,1857],{"class":74},"voip_sip\n",[57,1859,1860,1862],{"class":59,"line":91},[57,1861,1274],{"class":1251},[57,1863,1255],{"class":254},[57,1865,1866,1868,1871,1873],{"class":59,"line":98},[57,1867,1281],{"class":254},[57,1869,1870],{"class":1251},"alert",[57,1872,1266],{"class":254},[57,1874,1875],{"class":74},"HighSIP5xxRate\n",[57,1877,1878,1880,1882],{"class":59,"line":123},[57,1879,1294],{"class":1251},[57,1881,1266],{"class":254},[57,1883,1884],{"class":84},"|\n",[57,1886,1887],{"class":59,"line":132},[57,1888,1889],{"class":74},"          rate(kamailio_core_rcv_replies_total{status=~\"5..\"}[5m])\n",[57,1891,1892],{"class":59,"line":143},[57,1893,1894],{"class":74},"          \u002F rate(kamailio_core_rcv_replies_total[5m]) > 0.05\n",[57,1896,1897,1900,1902],{"class":59,"line":148},[57,1898,1899],{"class":1251},"        for",[57,1901,1266],{"class":254},[57,1903,1904],{"class":74},"3m\n",[57,1906,1907,1910],{"class":59,"line":154},[57,1908,1909],{"class":1251},"        labels",[57,1911,1255],{"class":254},[57,1913,1914,1917,1919],{"class":59,"line":175},[57,1915,1916],{"class":1251},"          severity",[57,1918,1266],{"class":254},[57,1920,1921],{"class":74},"critical\n",[57,1923,1924,1927,1929],{"class":59,"line":190},[57,1925,1926],{"class":1251},"          team",[57,1928,1266],{"class":254},[57,1930,1931],{"class":74},"voip\n",[57,1933,1934,1937],{"class":59,"line":195},[57,1935,1936],{"class":1251},"        annotations",[57,1938,1255],{"class":254},[57,1940,1941,1944,1946],{"class":59,"line":207},[57,1942,1943],{"class":1251},"          summary",[57,1945,1266],{"class":254},[57,1947,1948],{"class":74},"\"SIP 5xx rate {{ $value | humanizePercentage }} on {{ $labels.instance }}\"\n",[57,1950,1951,1954,1956],{"class":59,"line":216},[57,1952,1953],{"class":1251},"          runbook",[57,1955,1266],{"class":254},[57,1957,1958],{"class":74},"\"https:\u002F\u002Fwiki.example.com\u002Frunbooks\u002Fsip-5xx\"\n",[57,1960,1961],{"class":59,"line":222},[57,1962,95],{"emptyLinePlaceholder":94},[57,1964,1965,1967,1969,1971],{"class":59,"line":490},[57,1966,1281],{"class":254},[57,1968,1870],{"class":1251},[57,1970,1266],{"class":254},[57,1972,1973],{"class":74},"KamailioDialogsHigh\n",[57,1975,1976,1978,1980],{"class":59,"line":496},[57,1977,1294],{"class":1251},[57,1979,1266],{"class":254},[57,1981,1982],{"class":74},"kamailio:active_dialogs > 8000\n",[57,1984,1985,1987,1989],{"class":59,"line":502},[57,1986,1899],{"class":1251},[57,1988,1266],{"class":254},[57,1990,1991],{"class":74},"5m\n",[57,1993,1994,1996],{"class":59,"line":507},[57,1995,1909],{"class":1251},[57,1997,1255],{"class":254},[57,1999,2000,2002,2004],{"class":59,"line":513},[57,2001,1916],{"class":1251},[57,2003,1266],{"class":254},[57,2005,2006],{"class":74},"warning\n",[57,2008,2009,2011],{"class":59,"line":519},[57,2010,1936],{"class":1251},[57,2012,1255],{"class":254},[57,2014,2015,2017,2019],{"class":59,"line":525},[57,2016,1943],{"class":1251},[57,2018,1266],{"class":254},[57,2020,2021],{"class":74},"\"Active dialogs approaching capacity: {{ $value }}\"\n",[57,2023,2024],{"class":59,"line":531},[57,2025,95],{"emptyLinePlaceholder":94},[57,2027,2028,2030,2032,2034],{"class":59,"line":536},[57,2029,1281],{"class":254},[57,2031,1870],{"class":1251},[57,2033,1266],{"class":254},[57,2035,2036],{"class":74},"RegistrationFailureSpike\n",[57,2038,2039,2041,2043],{"class":59,"line":542},[57,2040,1294],{"class":1251},[57,2042,1266],{"class":254},[57,2044,2045],{"class":74},"kamailio:register_failure_rate > 0.2\n",[57,2047,2048,2050,2052],{"class":59,"line":548},[57,2049,1899],{"class":1251},[57,2051,1266],{"class":254},[57,2053,2054],{"class":74},"2m\n",[57,2056,2057,2059],{"class":59,"line":554},[57,2058,1909],{"class":1251},[57,2060,1255],{"class":254},[57,2062,2063,2065,2067],{"class":59,"line":560},[57,2064,1916],{"class":1251},[57,2066,1266],{"class":254},[57,2068,1921],{"class":74},[57,2070,2071,2073],{"class":59,"line":565},[57,2072,1936],{"class":1251},[57,2074,1255],{"class":254},[57,2076,2077,2079,2081],{"class":59,"line":571},[57,2078,1943],{"class":1251},[57,2080,1266],{"class":254},[57,2082,2083],{"class":74},"\"20%+ of SIP registrations failing — possible auth issue or attack\"\n",[57,2085,2086],{"class":59,"line":577},[57,2087,95],{"emptyLinePlaceholder":94},[57,2089,2090,2092,2094,2096],{"class":59,"line":582},[57,2091,1260],{"class":254},[57,2093,1263],{"class":1251},[57,2095,1266],{"class":254},[57,2097,2098],{"class":74},"voip_media\n",[57,2100,2101,2103],{"class":59,"line":588},[57,2102,1274],{"class":1251},[57,2104,1255],{"class":254},[57,2106,2107,2109,2111,2113],{"class":59,"line":594},[57,2108,1281],{"class":254},[57,2110,1870],{"class":1251},[57,2112,1266],{"class":254},[57,2114,2115],{"class":74},"MediaQualityDegraded\n",[57,2117,2118,2120,2122],{"class":59,"line":600},[57,2119,1294],{"class":1251},[57,2121,1266],{"class":254},[57,2123,2124],{"class":74},"rtpengine_mos_score \u003C 3.5\n",[57,2126,2127,2129,2131],{"class":59,"line":605},[57,2128,1899],{"class":1251},[57,2130,1266],{"class":254},[57,2132,1991],{"class":74},[57,2134,2135,2137],{"class":59,"line":611},[57,2136,1909],{"class":1251},[57,2138,1255],{"class":254},[57,2140,2141,2143,2145],{"class":59,"line":617},[57,2142,1916],{"class":1251},[57,2144,1266],{"class":254},[57,2146,2006],{"class":74},[57,2148,2149,2151],{"class":59,"line":623},[57,2150,1936],{"class":1251},[57,2152,1255],{"class":254},[57,2154,2156,2158,2160],{"class":59,"line":2155},41,[57,2157,1943],{"class":1251},[57,2159,1266],{"class":254},[57,2161,2162],{"class":74},"\"MOS score {{ $value }} below 3.5 on {{ $labels.instance }}\"\n",[57,2164,2166],{"class":59,"line":2165},42,[57,2167,95],{"emptyLinePlaceholder":94},[57,2169,2171,2173,2175,2177],{"class":59,"line":2170},43,[57,2172,1281],{"class":254},[57,2174,1870],{"class":1251},[57,2176,1266],{"class":254},[57,2178,2179],{"class":74},"MediaPacketLossHigh\n",[57,2181,2183,2185,2187],{"class":59,"line":2182},44,[57,2184,1294],{"class":1251},[57,2186,1266],{"class":254},[57,2188,2189],{"class":74},"rtpengine_packet_loss_ratio > 0.03\n",[57,2191,2193,2195,2197],{"class":59,"line":2192},45,[57,2194,1899],{"class":1251},[57,2196,1266],{"class":254},[57,2198,1904],{"class":74},[57,2200,2202,2204],{"class":59,"line":2201},46,[57,2203,1909],{"class":1251},[57,2205,1255],{"class":254},[57,2207,2209,2211,2213],{"class":59,"line":2208},47,[57,2210,1916],{"class":1251},[57,2212,1266],{"class":254},[57,2214,1921],{"class":74},[57,2216,2218,2220],{"class":59,"line":2217},48,[57,2219,1936],{"class":1251},[57,2221,1255],{"class":254},[57,2223,2225,2227,2229],{"class":59,"line":2224},49,[57,2226,1943],{"class":1251},[57,2228,1266],{"class":254},[57,2230,2231],{"class":74},"\"RTP packet loss {{ $value | humanizePercentage }} — calls impacted\"\n",[57,2233,2235],{"class":59,"line":2234},50,[57,2236,95],{"emptyLinePlaceholder":94},[57,2238,2240,2242,2244,2246],{"class":59,"line":2239},51,[57,2241,1281],{"class":254},[57,2243,1870],{"class":1251},[57,2245,1266],{"class":254},[57,2247,2248],{"class":74},"rtpengineCapacityHigh\n",[57,2250,2252,2254,2256],{"class":59,"line":2251},52,[57,2253,1294],{"class":1251},[57,2255,1266],{"class":254},[57,2257,2258],{"class":74},"rtpengine_active_sessions \u002F rtpengine_max_sessions > 0.85\n",[57,2260,2262,2264,2266],{"class":59,"line":2261},53,[57,2263,1899],{"class":1251},[57,2265,1266],{"class":254},[57,2267,1991],{"class":74},[57,2269,2271,2273],{"class":59,"line":2270},54,[57,2272,1909],{"class":1251},[57,2274,1255],{"class":254},[57,2276,2278,2280,2282],{"class":59,"line":2277},55,[57,2279,1916],{"class":1251},[57,2281,1266],{"class":254},[57,2283,2006],{"class":74},[57,2285,2287,2289],{"class":59,"line":2286},56,[57,2288,1936],{"class":1251},[57,2290,1255],{"class":254},[57,2292,2294,2296,2298],{"class":59,"line":2293},57,[57,2295,1943],{"class":1251},[57,2297,1266],{"class":254},[57,2299,2300],{"class":74},"\"rtpengine at {{ $value | humanizePercentage }} capacity\"\n",[20,2302,2304],{"id":2303},"grafana-dashboard-layout","Grafana Dashboard Layout",[16,2306,2307],{},"Structure your Grafana dashboard in four rows:",[16,2309,2310],{},[2311,2312,2313],"strong",{},"Row 1: SIP Signaling Health",[2315,2316,2317,2320,2323,2326],"ul",{},[788,2318,2319],{},"INVITE rate (calls\u002Fsec) — line graph, 1h window",[788,2321,2322],{},"SIP 4xx\u002F5xx rate — stat panel with threshold coloring",[788,2324,2325],{},"Active dialogs — gauge panel",[788,2327,2328],{},"Registration success rate — stat panel",[16,2330,2331],{},[2311,2332,2333],{},"Row 2: Media Quality",[2315,2335,2336,2339,2342,2345],{},[788,2337,2338],{},"MOS score distribution by trunk — heatmap",[788,2340,2341],{},"Packet loss % by carrier — time series",[788,2343,2344],{},"Jitter ms — time series with threshold line at 50ms",[788,2346,2347],{},"Active RTP sessions — gauge",[16,2349,2350],{},[2311,2351,2352],{},"Row 3: Infrastructure",[2315,2354,2355,2358,2361],{},[788,2356,2357],{},"CPU per VoIP node — multi-series line",[788,2359,2360],{},"Network I\u002FO (bytes\u002Fsec) — time series",[788,2362,2363],{},"Memory usage — time series",[16,2365,2366],{},[2311,2367,2368],{},"Row 4: Business Metrics",[2315,2370,2371,2374,2377,2380],{},[788,2372,2373],{},"ASR by trunk — bar gauge",[788,2375,2376],{},"ACD (average call duration) — stat panel",[788,2378,2379],{},"Total calls in last 24h — stat panel",[788,2381,2382],{},"Calls by outcome (Answered\u002FNo Answer\u002FBusy) — pie chart",[20,2384,2386],{"id":2385},"prometheus-scrape-configuration","Prometheus Scrape Configuration",[48,2388,2390],{"className":1237,"code":2389,"language":1239,"meta":53,"style":53},"# prometheus.yml\nscrape_configs:\n  - job_name: 'kamailio'\n    static_configs:\n      - targets: ['kamailio-1:9494', 'kamailio-2:9494']\n    scrape_interval: 10s\n\n  - job_name: 'asterisk'\n    static_configs:\n      - targets: ['asterisk-1:9200', 'asterisk-2:9200']\n    scrape_interval: 30s\n\n  - job_name: 'rtpengine'\n    static_configs:\n      - targets: ['rtpengine-1:9900', 'rtpengine-2:9900']\n    scrape_interval: 10s\n\n  - job_name: 'coturn'\n    static_configs:\n      - targets: ['turn-1:9641']\n    scrape_interval: 30s\n\n  - job_name: 'node'\n    static_configs:\n      - targets: ['kamailio-1:9100', 'asterisk-1:9100', 'rtpengine-1:9100']\n    scrape_interval: 15s\n",[28,2391,2392,2397,2404,2416,2423,2445,2455,2459,2470,2476,2494,2503,2507,2518,2524,2542,2550,2554,2565,2571,2584,2592,2596,2607,2613,2636],{"__ignoreMap":53},[57,2393,2394],{"class":59,"line":60},[57,2395,2396],{"class":63},"# prometheus.yml\n",[57,2398,2399,2402],{"class":59,"line":67},[57,2400,2401],{"class":1251},"scrape_configs",[57,2403,1255],{"class":254},[57,2405,2406,2408,2411,2413],{"class":59,"line":81},[57,2407,1260],{"class":254},[57,2409,2410],{"class":1251},"job_name",[57,2412,1266],{"class":254},[57,2414,2415],{"class":74},"'kamailio'\n",[57,2417,2418,2421],{"class":59,"line":91},[57,2419,2420],{"class":1251},"    static_configs",[57,2422,1255],{"class":254},[57,2424,2425,2427,2430,2433,2436,2439,2442],{"class":59,"line":98},[57,2426,1281],{"class":254},[57,2428,2429],{"class":1251},"targets",[57,2431,2432],{"class":254},": [",[57,2434,2435],{"class":74},"'kamailio-1:9494'",[57,2437,2438],{"class":254},", ",[57,2440,2441],{"class":74},"'kamailio-2:9494'",[57,2443,2444],{"class":254},"]\n",[57,2446,2447,2450,2452],{"class":59,"line":123},[57,2448,2449],{"class":1251},"    scrape_interval",[57,2451,1266],{"class":254},[57,2453,2454],{"class":74},"10s\n",[57,2456,2457],{"class":59,"line":132},[57,2458,95],{"emptyLinePlaceholder":94},[57,2460,2461,2463,2465,2467],{"class":59,"line":143},[57,2462,1260],{"class":254},[57,2464,2410],{"class":1251},[57,2466,1266],{"class":254},[57,2468,2469],{"class":74},"'asterisk'\n",[57,2471,2472,2474],{"class":59,"line":148},[57,2473,2420],{"class":1251},[57,2475,1255],{"class":254},[57,2477,2478,2480,2482,2484,2487,2489,2492],{"class":59,"line":154},[57,2479,1281],{"class":254},[57,2481,2429],{"class":1251},[57,2483,2432],{"class":254},[57,2485,2486],{"class":74},"'asterisk-1:9200'",[57,2488,2438],{"class":254},[57,2490,2491],{"class":74},"'asterisk-2:9200'",[57,2493,2444],{"class":254},[57,2495,2496,2498,2500],{"class":59,"line":175},[57,2497,2449],{"class":1251},[57,2499,1266],{"class":254},[57,2501,2502],{"class":74},"30s\n",[57,2504,2505],{"class":59,"line":190},[57,2506,95],{"emptyLinePlaceholder":94},[57,2508,2509,2511,2513,2515],{"class":59,"line":195},[57,2510,1260],{"class":254},[57,2512,2410],{"class":1251},[57,2514,1266],{"class":254},[57,2516,2517],{"class":74},"'rtpengine'\n",[57,2519,2520,2522],{"class":59,"line":207},[57,2521,2420],{"class":1251},[57,2523,1255],{"class":254},[57,2525,2526,2528,2530,2532,2535,2537,2540],{"class":59,"line":216},[57,2527,1281],{"class":254},[57,2529,2429],{"class":1251},[57,2531,2432],{"class":254},[57,2533,2534],{"class":74},"'rtpengine-1:9900'",[57,2536,2438],{"class":254},[57,2538,2539],{"class":74},"'rtpengine-2:9900'",[57,2541,2444],{"class":254},[57,2543,2544,2546,2548],{"class":59,"line":222},[57,2545,2449],{"class":1251},[57,2547,1266],{"class":254},[57,2549,2454],{"class":74},[57,2551,2552],{"class":59,"line":490},[57,2553,95],{"emptyLinePlaceholder":94},[57,2555,2556,2558,2560,2562],{"class":59,"line":496},[57,2557,1260],{"class":254},[57,2559,2410],{"class":1251},[57,2561,1266],{"class":254},[57,2563,2564],{"class":74},"'coturn'\n",[57,2566,2567,2569],{"class":59,"line":502},[57,2568,2420],{"class":1251},[57,2570,1255],{"class":254},[57,2572,2573,2575,2577,2579,2582],{"class":59,"line":507},[57,2574,1281],{"class":254},[57,2576,2429],{"class":1251},[57,2578,2432],{"class":254},[57,2580,2581],{"class":74},"'turn-1:9641'",[57,2583,2444],{"class":254},[57,2585,2586,2588,2590],{"class":59,"line":513},[57,2587,2449],{"class":1251},[57,2589,1266],{"class":254},[57,2591,2502],{"class":74},[57,2593,2594],{"class":59,"line":519},[57,2595,95],{"emptyLinePlaceholder":94},[57,2597,2598,2600,2602,2604],{"class":59,"line":525},[57,2599,1260],{"class":254},[57,2601,2410],{"class":1251},[57,2603,1266],{"class":254},[57,2605,2606],{"class":74},"'node'\n",[57,2608,2609,2611],{"class":59,"line":531},[57,2610,2420],{"class":1251},[57,2612,1255],{"class":254},[57,2614,2615,2617,2619,2621,2624,2626,2629,2631,2634],{"class":59,"line":536},[57,2616,1281],{"class":254},[57,2618,2429],{"class":1251},[57,2620,2432],{"class":254},[57,2622,2623],{"class":74},"'kamailio-1:9100'",[57,2625,2438],{"class":254},[57,2627,2628],{"class":74},"'asterisk-1:9100'",[57,2630,2438],{"class":254},[57,2632,2633],{"class":74},"'rtpengine-1:9100'",[57,2635,2444],{"class":254},[57,2637,2638,2640,2642],{"class":59,"line":542},[57,2639,2449],{"class":1251},[57,2641,1266],{"class":254},[57,2643,2644],{"class":74},"15s\n",[20,2646,2648],{"id":2647},"storage-sizing-for-voip-metrics","Storage Sizing for VoIP Metrics",[16,2650,2651],{},"VoIP monitoring generates high-cardinality metrics — per-call, per-trunk, per-carrier labels multiply metric series. Calculate your Prometheus storage requirements:",[2315,2653,2654,2657,2660,2663,2666,2669,2672],{},[788,2655,2656],{},"Samples per scrape: ~500 (typical VoIP stack)",[788,2658,2659],{},"Scrape interval: 10s → 6 scrapes\u002Fminute",[788,2661,2662],{},"Samples\u002Fminute: 3,000",[788,2664,2665],{},"Samples\u002Fday: 4,320,000",[788,2667,2668],{},"Prometheus bytes per sample: ~1.5 bytes (compressed)",[788,2670,2671],{},"Storage\u002Fday: ~6 MB",[788,2673,2674],{},"90-day retention: ~540 MB",[16,2676,2677],{},"This fits comfortably on any VPS. For longer retention or higher cardinality (1,000+ trunks), use Thanos or Mimir to offload to object storage and query across retention windows.",[1009,2679,2680],{},"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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":53,"searchDepth":67,"depth":67,"links":2682},[2683,2684,2685,2689,2690,2691,2692,2693],{"id":1053,"depth":67,"text":1054},{"id":1124,"depth":67,"text":1125},{"id":1379,"depth":67,"text":1380,"children":2686},[2687,2688],{"id":1387,"depth":81,"text":1388},{"id":1529,"depth":81,"text":1530},{"id":1709,"depth":67,"text":1710},{"id":1829,"depth":67,"text":1830},{"id":2303,"depth":67,"text":2304},{"id":2385,"depth":67,"text":2386},{"id":2647,"depth":67,"text":2648},"Architecture","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1504868584819-f8e8b4b6d7e3?w=1200&q=80","2026-02-01","Build a production VoIP observability stack with Prometheus and Grafana: Kamailio stats, Asterisk metrics via snmp-exporter, RTP quality dashboards, and SLA alerting rules.",{},"\u002Fblog\u002Fvoip-monitoring-prometheus-grafana",{"title":1042,"description":2697},"blog\u002Fvoip-monitoring-prometheus-grafana",[2703,2704,2705,2706,1036,2707,2708],"prometheus","grafana","voip-monitoring","observability","asterisk","rtp","IEEtiIKoFkLLUEfqYfeAeFjDBfS_VO2b8QZL6Zu7Jso",{"id":2711,"title":2712,"author":7,"body":2713,"category":3495,"coverImage":3496,"date":3497,"description":3498,"extension":1027,"meta":3499,"navigation":94,"path":3500,"readingTime":175,"seo":3501,"stem":3502,"tags":3503,"__hash__":3508},"posts\u002Fblog\u002Fwebrtc-media-server-comparison.md","LiveKit vs Janus vs Mediasoup: Choosing a WebRTC Media Server",{"type":9,"value":2714,"toc":3479},[2715,2718,2721,2725,2728,2732,2735,2738,2742,2745,2748,2752,2755,2758,2762,2765,2866,2869,2873,2876,2880,2883,2962,2966,2969,3131,3134,3138,3222,3225,3229,3327,3330,3334,3339,3353,3358,3372,3377,3391,3395,3398,3473,3476],[12,2716,2712],{"id":2717},"livekit-vs-janus-vs-mediasoup-choosing-a-webrtc-media-server",[16,2719,2720],{},"Selecting a WebRTC media server is a decision that affects your system architecture for years. The wrong choice means rewriting your signaling layer, renegotiating your infrastructure costs, or hitting scaling walls at 500 users instead of 50,000. LiveKit, Janus, and Mediasoup each represent a different point in the design space — developer experience versus flexibility versus raw performance. This post compares them on the dimensions that matter in production.",[20,2722,2724],{"id":2723},"architecture-fundamentals","Architecture Fundamentals",[16,2726,2727],{},"All three are Selective Forwarding Units (SFUs). An SFU receives each participant's media streams and selectively forwards them to other participants without transcoding. This is the right architecture for multi-party conferencing: it scales to hundreds of participants without the CPU overhead of an MCU (mixing server).",[1385,2729,2731],{"id":2730},"livekit","LiveKit",[16,2733,2734],{},"LiveKit is a Go-based SFU with opinionated signaling built on Protocol Buffers over WebSocket. It ships as a single binary with built-in room management, participant state, data channels, and egress\u002Fingress pipelines. LiveKit Cloud is the managed offering; self-hosted LiveKit is Apache 2.0 licensed.",[16,2736,2737],{},"Architecture: stateless SFU nodes behind a Redis-backed cluster coordinator. Each node handles media independently; Redis propagates room state changes across nodes. Horizontal scaling requires only adding nodes — no configuration changes.",[1385,2739,2741],{"id":2740},"janus","Janus",[16,2743,2744],{},"Janus (by Meetecho) is a C-based general-purpose WebRTC gateway with a plugin architecture. It does not implement SFU semantics natively — instead, it exposes a low-level API where each plugin handles a specific use case (VideoRoom for conferencing, SIPGateway for SIP bridging, AudioBridge for audio mixing). Signaling is flexible: JSON over WebSocket, HTTP, or MQTT.",[16,2746,2747],{},"Architecture: monolithic single-process model. Scaling requires multiple Janus instances behind a load balancer, with application-level logic to route participants in the same room to the same instance.",[1385,2749,2751],{"id":2750},"mediasoup","Mediasoup",[16,2753,2754],{},"Mediasoup is a Node.js library (with a C++ worker process) that gives you raw WebRTC transport primitives — Producers, Consumers, Routers, Transports — without any signaling or room management. You build everything above the transport layer. It is the most flexible and the most work.",[16,2756,2757],{},"Architecture: single-host by default. Mediasoup's Router runs on one Worker process (one CPU core). Multi-core requires spawning multiple Workers and routing participants across them — your application code manages which Worker handles which participant.",[20,2759,2761],{"id":2760},"performance-comparison","Performance Comparison",[16,2763,2764],{},"Benchmark environment: 4-core \u002F 16 GB VM, VP8 video at 1 Mbps, 720p 30fps, all participants sending video.",[661,2766,2767,2780],{},[664,2768,2769],{},[667,2770,2771,2773,2775,2778],{},[670,2772,1750],{},[670,2774,2731],{},[670,2776,2777],{},"Janus (VideoRoom)",[670,2779,2751],{},[677,2781,2782,2796,2810,2824,2838,2852],{},[667,2783,2784,2787,2790,2793],{},[682,2785,2786],{},"Max participants (4 cores)",[682,2788,2789],{},"~300",[682,2791,2792],{},"~150",[682,2794,2795],{},"~400",[667,2797,2798,2801,2804,2807],{},[682,2799,2800],{},"CPU per 10 participants",[682,2802,2803],{},"8%",[682,2805,2806],{},"15%",[682,2808,2809],{},"6%",[667,2811,2812,2815,2818,2821],{},[682,2813,2814],{},"Latency (p50, same DC)",[682,2816,2817],{},"45ms",[682,2819,2820],{},"65ms",[682,2822,2823],{},"40ms",[667,2825,2826,2829,2832,2835],{},[682,2827,2828],{},"Latency (p99, same DC)",[682,2830,2831],{},"110ms",[682,2833,2834],{},"180ms",[682,2836,2837],{},"95ms",[667,2839,2840,2843,2846,2849],{},[682,2841,2842],{},"Memory per participant",[682,2844,2845],{},"8 MB",[682,2847,2848],{},"12 MB",[682,2850,2851],{},"6 MB",[667,2853,2854,2857,2860,2863],{},[682,2855,2856],{},"Time to first frame",[682,2858,2859],{},"380ms",[682,2861,2862],{},"520ms",[682,2864,2865],{},"310ms",[16,2867,2868],{},"Mediasoup has the best raw performance because the C++ Worker does minimal work beyond forwarding — no room management, no signaling overhead, no state synchronization. LiveKit is close and adds significantly more operational value. Janus pays a C plugin overhead and is bottlenecked by its single-process architecture at high concurrency.",[20,2870,2872],{"id":2871},"signaling-complexity","Signaling Complexity",[16,2874,2875],{},"This is the starker difference than performance.",[1385,2877,2879],{"id":2878},"livekit-signaling","LiveKit Signaling",[16,2881,2882],{},"LiveKit handles signaling. You call a room API and the SDK manages ICE, DTLS, offer\u002Fanswer negotiation, and track subscription automatically.",[48,2884,2888],{"className":2885,"code":2886,"language":2887,"meta":53,"style":53},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F LiveKit — 20 lines to publish and subscribe\nimport { Room, RoomEvent, Track } from 'livekit-client';\n\nconst room = new Room();\nawait room.connect('wss:\u002F\u002Fyour-livekit-server.com', token);\n\n\u002F\u002F Publish camera\nconst localTrack = await createLocalVideoTrack();\nawait room.localParticipant.publishTrack(localTrack);\n\n\u002F\u002F Subscribe to remote tracks automatically\nroom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {\n  const element = track.attach();\n  document.body.appendChild(element);\n});\n","javascript",[28,2889,2890,2895,2900,2904,2909,2914,2918,2923,2928,2933,2937,2942,2947,2952,2957],{"__ignoreMap":53},[57,2891,2892],{"class":59,"line":60},[57,2893,2894],{},"\u002F\u002F LiveKit — 20 lines to publish and subscribe\n",[57,2896,2897],{"class":59,"line":67},[57,2898,2899],{},"import { Room, RoomEvent, Track } from 'livekit-client';\n",[57,2901,2902],{"class":59,"line":81},[57,2903,95],{"emptyLinePlaceholder":94},[57,2905,2906],{"class":59,"line":91},[57,2907,2908],{},"const room = new Room();\n",[57,2910,2911],{"class":59,"line":98},[57,2912,2913],{},"await room.connect('wss:\u002F\u002Fyour-livekit-server.com', token);\n",[57,2915,2916],{"class":59,"line":123},[57,2917,95],{"emptyLinePlaceholder":94},[57,2919,2920],{"class":59,"line":132},[57,2921,2922],{},"\u002F\u002F Publish camera\n",[57,2924,2925],{"class":59,"line":143},[57,2926,2927],{},"const localTrack = await createLocalVideoTrack();\n",[57,2929,2930],{"class":59,"line":148},[57,2931,2932],{},"await room.localParticipant.publishTrack(localTrack);\n",[57,2934,2935],{"class":59,"line":154},[57,2936,95],{"emptyLinePlaceholder":94},[57,2938,2939],{"class":59,"line":175},[57,2940,2941],{},"\u002F\u002F Subscribe to remote tracks automatically\n",[57,2943,2944],{"class":59,"line":190},[57,2945,2946],{},"room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {\n",[57,2948,2949],{"class":59,"line":195},[57,2950,2951],{},"  const element = track.attach();\n",[57,2953,2954],{"class":59,"line":207},[57,2955,2956],{},"  document.body.appendChild(element);\n",[57,2958,2959],{"class":59,"line":216},[57,2960,2961],{},"});\n",[1385,2963,2965],{"id":2964},"mediasoup-signaling","Mediasoup Signaling",[16,2967,2968],{},"Mediasoup gives you transports. Your signaling server (WebSocket, SIP, whatever you want) is entirely your responsibility.",[48,2970,2972],{"className":2885,"code":2971,"language":2887,"meta":53,"style":53},"\u002F\u002F Mediasoup server-side — create transports and handle signaling manually\nconst worker = await mediasoup.createWorker();\nconst router = await worker.createRouter({ mediaCodecs });\n\n\u002F\u002F For each participant joining:\nconst transport = await router.createWebRtcTransport({\n  listenIps: [{ ip: '0.0.0.0', announcedIp: '203.0.113.1' }],\n  enableUdp: true,\n  enableTcp: true,\n  preferUdp: true,\n});\n\n\u002F\u002F Send transport parameters to client via YOUR signaling channel\nsocket.emit('transport-created', {\n  id: transport.id,\n  iceParameters: transport.iceParameters,\n  iceCandidates: transport.iceCandidates,\n  dtlsParameters: transport.dtlsParameters,\n});\n\n\u002F\u002F Handle connect from client\nsocket.on('transport-connect', async ({ dtlsParameters }) => {\n  await transport.connect({ dtlsParameters });\n});\n\n\u002F\u002F Handle produce from client\nsocket.on('produce', async ({ kind, rtpParameters }, callback) => {\n  const producer = await transport.produce({ kind, rtpParameters });\n  callback({ id: producer.id });\n  \n  \u002F\u002F Route to consumers (other participants) — your logic\n  broadcastNewProducer(producer.id, router);\n});\n",[28,2973,2974,2979,2984,2989,2993,2998,3003,3008,3013,3018,3023,3027,3031,3036,3041,3046,3051,3056,3061,3065,3069,3074,3079,3084,3088,3092,3097,3102,3107,3112,3117,3122,3127],{"__ignoreMap":53},[57,2975,2976],{"class":59,"line":60},[57,2977,2978],{},"\u002F\u002F Mediasoup server-side — create transports and handle signaling manually\n",[57,2980,2981],{"class":59,"line":67},[57,2982,2983],{},"const worker = await mediasoup.createWorker();\n",[57,2985,2986],{"class":59,"line":81},[57,2987,2988],{},"const router = await worker.createRouter({ mediaCodecs });\n",[57,2990,2991],{"class":59,"line":91},[57,2992,95],{"emptyLinePlaceholder":94},[57,2994,2995],{"class":59,"line":98},[57,2996,2997],{},"\u002F\u002F For each participant joining:\n",[57,2999,3000],{"class":59,"line":123},[57,3001,3002],{},"const transport = await router.createWebRtcTransport({\n",[57,3004,3005],{"class":59,"line":132},[57,3006,3007],{},"  listenIps: [{ ip: '0.0.0.0', announcedIp: '203.0.113.1' }],\n",[57,3009,3010],{"class":59,"line":143},[57,3011,3012],{},"  enableUdp: true,\n",[57,3014,3015],{"class":59,"line":148},[57,3016,3017],{},"  enableTcp: true,\n",[57,3019,3020],{"class":59,"line":154},[57,3021,3022],{},"  preferUdp: true,\n",[57,3024,3025],{"class":59,"line":175},[57,3026,2961],{},[57,3028,3029],{"class":59,"line":190},[57,3030,95],{"emptyLinePlaceholder":94},[57,3032,3033],{"class":59,"line":195},[57,3034,3035],{},"\u002F\u002F Send transport parameters to client via YOUR signaling channel\n",[57,3037,3038],{"class":59,"line":207},[57,3039,3040],{},"socket.emit('transport-created', {\n",[57,3042,3043],{"class":59,"line":216},[57,3044,3045],{},"  id: transport.id,\n",[57,3047,3048],{"class":59,"line":222},[57,3049,3050],{},"  iceParameters: transport.iceParameters,\n",[57,3052,3053],{"class":59,"line":490},[57,3054,3055],{},"  iceCandidates: transport.iceCandidates,\n",[57,3057,3058],{"class":59,"line":496},[57,3059,3060],{},"  dtlsParameters: transport.dtlsParameters,\n",[57,3062,3063],{"class":59,"line":502},[57,3064,2961],{},[57,3066,3067],{"class":59,"line":507},[57,3068,95],{"emptyLinePlaceholder":94},[57,3070,3071],{"class":59,"line":513},[57,3072,3073],{},"\u002F\u002F Handle connect from client\n",[57,3075,3076],{"class":59,"line":519},[57,3077,3078],{},"socket.on('transport-connect', async ({ dtlsParameters }) => {\n",[57,3080,3081],{"class":59,"line":525},[57,3082,3083],{},"  await transport.connect({ dtlsParameters });\n",[57,3085,3086],{"class":59,"line":531},[57,3087,2961],{},[57,3089,3090],{"class":59,"line":536},[57,3091,95],{"emptyLinePlaceholder":94},[57,3093,3094],{"class":59,"line":542},[57,3095,3096],{},"\u002F\u002F Handle produce from client\n",[57,3098,3099],{"class":59,"line":548},[57,3100,3101],{},"socket.on('produce', async ({ kind, rtpParameters }, callback) => {\n",[57,3103,3104],{"class":59,"line":554},[57,3105,3106],{},"  const producer = await transport.produce({ kind, rtpParameters });\n",[57,3108,3109],{"class":59,"line":560},[57,3110,3111],{},"  callback({ id: producer.id });\n",[57,3113,3114],{"class":59,"line":565},[57,3115,3116],{},"  \n",[57,3118,3119],{"class":59,"line":571},[57,3120,3121],{},"  \u002F\u002F Route to consumers (other participants) — your logic\n",[57,3123,3124],{"class":59,"line":577},[57,3125,3126],{},"  broadcastNewProducer(producer.id, router);\n",[57,3128,3129],{"class":59,"line":582},[57,3130,2961],{},[16,3132,3133],{},"Mediasoup requires 500–800 lines of server-side signaling code for a basic conferencing app. LiveKit requires roughly 50. Janus sits in between — its REST API handles room creation and participant management, but you still write the WebRTC negotiation logic in your client.",[20,3135,3137],{"id":3136},"multi-server-clustering","Multi-Server Clustering",[661,3139,3140,3153],{},[664,3141,3142],{},[667,3143,3144,3147,3149,3151],{},[670,3145,3146],{},"Capability",[670,3148,2731],{},[670,3150,2741],{},[670,3152,2751],{},[677,3154,3155,3168,3181,3195,3208],{},[667,3156,3157,3160,3163,3166],{},[682,3158,3159],{},"Native clustering",[682,3161,3162],{},"Yes (Redis-backed)",[682,3164,3165],{},"No",[682,3167,3165],{},[667,3169,3170,3173,3176,3179],{},[682,3171,3172],{},"Cross-node participant routing",[682,3174,3175],{},"Automatic",[682,3177,3178],{},"Manual",[682,3180,3178],{},[667,3182,3183,3186,3189,3192],{},[682,3184,3185],{},"Horizontal scaling",[682,3187,3188],{},"Add nodes",[682,3190,3191],{},"App-level routing",[682,3193,3194],{},"Per-host Workers",[667,3196,3197,3200,3203,3206],{},[682,3198,3199],{},"Cloud-native (K8s)",[682,3201,3202],{},"First-class",[682,3204,3205],{},"Possible",[682,3207,3205],{},[667,3209,3210,3213,3216,3219],{},[682,3211,3212],{},"Egress (recording, streaming)",[682,3214,3215],{},"Built-in",[682,3217,3218],{},"Via plugin",[682,3220,3221],{},"Via pipeline",[16,3223,3224],{},"LiveKit's clustering story is its strongest differentiator against Janus and Mediasoup. Deploy 5 LiveKit nodes behind a load balancer, point them at the same Redis, and room participants automatically end up on the same node via the cluster coordinator. Janus and Mediasoup require your application to track which instance a room lives on and route participants accordingly — real engineering work that LiveKit gives you for free.",[20,3226,3228],{"id":3227},"sdk-and-ecosystem","SDK and Ecosystem",[661,3230,3231,3244],{},[664,3232,3233],{},[667,3234,3235,3238,3240,3242],{},[670,3236,3237],{},"SDK",[670,3239,2731],{},[670,3241,2741],{},[670,3243,2751],{},[677,3245,3246,3260,3272,3283,3294,3305,3316],{},[667,3247,3248,3251,3254,3257],{},[682,3249,3250],{},"JavaScript\u002FBrowser",[682,3252,3253],{},"Yes",[682,3255,3256],{},"Via Janus.js",[682,3258,3259],{},"Client-side only",[667,3261,3262,3265,3267,3270],{},[682,3263,3264],{},"iOS (Swift)",[682,3266,3253],{},[682,3268,3269],{},"Community",[682,3271,3165],{},[667,3273,3274,3277,3279,3281],{},[682,3275,3276],{},"Android (Kotlin)",[682,3278,3253],{},[682,3280,3269],{},[682,3282,3165],{},[667,3284,3285,3288,3290,3292],{},[682,3286,3287],{},"React Native",[682,3289,3253],{},[682,3291,3269],{},[682,3293,3165],{},[667,3295,3296,3299,3301,3303],{},[682,3297,3298],{},"Python (server)",[682,3300,3253],{},[682,3302,3165],{},[682,3304,3253],{},[667,3306,3307,3310,3312,3314],{},[682,3308,3309],{},"Go (server)",[682,3311,3253],{},[682,3313,3165],{},[682,3315,3165],{},[667,3317,3318,3321,3323,3325],{},[682,3319,3320],{},"Unity",[682,3322,3253],{},[682,3324,3165],{},[682,3326,3165],{},[16,3328,3329],{},"LiveKit invests heavily in client SDKs. For mobile applications, LiveKit is the clear choice — building native iOS and Android WebRTC clients without an SDK is weeks of work.",[20,3331,3333],{"id":3332},"use-case-decision-matrix","Use Case Decision Matrix",[16,3335,3336],{},[2311,3337,3338],{},"Choose LiveKit when:",[2315,3340,3341,3344,3347,3350],{},[788,3342,3343],{},"You need mobile SDK support (iOS, Android, React Native)",[788,3345,3346],{},"You want clustering and egress without custom infrastructure code",[788,3348,3349],{},"Your team is building fast and wants managed or self-hosted with minimal ops burden",[788,3351,3352],{},"You need simulcast, dynacast, and adaptive bitrate out of the box",[16,3354,3355],{},[2311,3356,3357],{},"Choose Janus when:",[2315,3359,3360,3363,3366,3369],{},[788,3361,3362],{},"You need SIP\u002FWebRTC bridging (Janus SIPGateway plugin)",[788,3364,3365],{},"You have specialized protocol requirements (RTMP, HLS ingest)",[788,3367,3368],{},"Your team has C expertise and wants to write custom Janus plugins",[788,3370,3371],{},"You're extending an existing Janus deployment",[16,3373,3374],{},[2311,3375,3376],{},"Choose Mediasoup when:",[2315,3378,3379,3382,3385,3388],{},[788,3380,3381],{},"Maximum performance per core is the constraint (streaming at scale, CDN POPs)",[788,3383,3384],{},"You need total control over signaling and have the engineering capacity to build it",[788,3386,3387],{},"You're integrating into an existing Node.js backend with specific signaling semantics",[788,3389,3390],{},"You're building specialized applications (low-latency auction, live betting, real-time gaming) where SFU primitives map directly to your domain model",[20,3392,3394],{"id":3393},"infrastructure-cost-estimate","Infrastructure Cost Estimate",[16,3396,3397],{},"At 10,000 daily active users, 30-minute average session, 5 participants per room average:",[661,3399,3400,3416],{},[664,3401,3402],{},[667,3403,3404,3407,3410,3413],{},[670,3405,3406],{},"Platform",[670,3408,3409],{},"Infra cost\u002Fmonth",[670,3411,3412],{},"Engineering cost to build",[670,3414,3415],{},"Ongoing maintenance",[677,3417,3418,3432,3445,3459],{},[667,3419,3420,3423,3426,3429],{},[682,3421,3422],{},"LiveKit Cloud",[682,3424,3425],{},"~$800",[682,3427,3428],{},"Low (days)",[682,3430,3431],{},"Minimal",[667,3433,3434,3437,3440,3442],{},[682,3435,3436],{},"LiveKit self-hosted",[682,3438,3439],{},"~$400",[682,3441,3428],{},[682,3443,3444],{},"Low",[667,3446,3447,3450,3453,3456],{},[682,3448,3449],{},"Janus self-hosted",[682,3451,3452],{},"~$350",[682,3454,3455],{},"Medium (weeks)",[682,3457,3458],{},"Medium",[667,3460,3461,3464,3467,3470],{},[682,3462,3463],{},"Mediasoup self-hosted",[682,3465,3466],{},"~$300",[682,3468,3469],{},"High (months)",[682,3471,3472],{},"High",[16,3474,3475],{},"The self-hosted Mediasoup cost advantage rarely outweighs the engineering investment unless you're operating at a scale where those infrastructure savings compound significantly (100,000+ DAU).",[1009,3477,3478],{},"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":53,"searchDepth":67,"depth":67,"links":3480},[3481,3486,3487,3491,3492,3493,3494],{"id":2723,"depth":67,"text":2724,"children":3482},[3483,3484,3485],{"id":2730,"depth":81,"text":2731},{"id":2740,"depth":81,"text":2741},{"id":2750,"depth":81,"text":2751},{"id":2760,"depth":67,"text":2761},{"id":2871,"depth":67,"text":2872,"children":3488},[3489,3490],{"id":2878,"depth":81,"text":2879},{"id":2964,"depth":81,"text":2965},{"id":3136,"depth":67,"text":3137},{"id":3227,"depth":67,"text":3228},{"id":3332,"depth":67,"text":3333},{"id":3393,"depth":67,"text":3394},"WebRTC","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1518770660439-4636190af475?w=1200&q=80","2026-01-01","Technical comparison of LiveKit, Janus, and Mediasoup WebRTC media servers: architecture, scalability, latency, SDK support, and which fits your use case best.",{},"\u002Fblog\u002Fwebrtc-media-server-comparison",{"title":2712,"description":3498},"blog\u002Fwebrtc-media-server-comparison",[3504,2730,2740,2750,3505,3506,3507],"webrtc","media-server","sfu","real-time","z3D7TdtYcvGwuB7Ily-x9C4gzlA6jEinIpS3uMSHgfA",{"id":3510,"title":3511,"author":7,"body":3512,"category":4563,"coverImage":4564,"date":4565,"description":4566,"extension":1027,"meta":4567,"navigation":94,"path":4568,"readingTime":143,"seo":4569,"stem":4570,"tags":4571,"__hash__":4578},"posts\u002Fblog\u002Fsip-trunking-stir-shaken.md","STIR\u002FSHAKEN Compliance for SIP Trunking Providers",{"type":9,"value":3513,"toc":4553},[3514,3517,3520,3523,3527,3533,3536,3540,3543,3593,3596,3600,3603,3617,3620,3735,3739,3742,3907,3910,3947,3951,4158,4162,4165,4171,4284,4287,4291,4294,4479,4482,4486,4547,4550],[12,3515,3511],{"id":3516},"stirshaken-compliance-for-sip-trunking-providers",[16,3518,3519],{},"STIR\u002FSHAKEN is the FCC-mandated caller ID authentication framework that SIP trunking providers must implement. STIR (Secure Telephone Identity Revisited) defines the cryptographic mechanism. SHAKEN (Signature-based Handling of Asserted information using toKENs) is the industry profile that specifies how US voice providers apply it. Since June 2021, major carriers require originating providers to sign calls or risk having traffic flagged as unverified — meaning customer calls display \"Spam Likely\" or get blocked by call-screening apps.",[16,3521,3522],{},"This post covers what SIP trunking providers need to implement: STI-SP certificate procurement, PASSporT generation, Identity header construction, and the verification flow on the terminating side.",[20,3524,3526],{"id":3525},"stirshaken-architecture","STIR\u002FSHAKEN Architecture",[48,3528,3531],{"className":3529,"code":3530,"language":654},[652],"Originating SP          Intermediate              Terminating SP\n(your platform)         (optional)                (PSTN carrier)\n\n    SIP INVITE ──────────────────────────────────────► SIP INVITE\n    + Identity header                                   + Identity header\n    (signed PASSporT)                                   (verified by carrier)\n           │\n           │ signs using\n           │\n    STI-CA certificate\n    (from approved CA list)\n",[28,3532,3530],{"__ignoreMap":53},[16,3534,3535],{},"The originating provider signs the call with an EC (Elliptic Curve) private key. The corresponding certificate, issued by an FCC-authorized STI-CA, is published at a public HTTPS URL. The terminating provider fetches the certificate and verifies the signature.",[20,3537,3539],{"id":3538},"attestation-levels","Attestation Levels",[16,3541,3542],{},"The Identity header includes an attestation level that signals how well the originating provider verified the caller:",[661,3544,3545,3558],{},[664,3546,3547],{},[667,3548,3549,3552,3555],{},[670,3550,3551],{},"Level",[670,3553,3554],{},"Code",[670,3556,3557],{},"Meaning",[677,3559,3560,3571,3582],{},[667,3561,3562,3565,3568],{},[682,3563,3564],{},"Full Attestation",[682,3566,3567],{},"A",[682,3569,3570],{},"You know the customer and the number is authorized to them",[667,3572,3573,3576,3579],{},[682,3574,3575],{},"Partial Attestation",[682,3577,3578],{},"B",[682,3580,3581],{},"You know the customer but cannot confirm they own the number",[667,3583,3584,3587,3590],{},[682,3585,3586],{},"Gateway Attestation",[682,3588,3589],{},"C",[682,3591,3592],{},"You authenticated the source but have no customer relationship",[16,3594,3595],{},"Use attestation A whenever possible. Carriers downgrade or reject calls with level C from unknown providers. If you're a transit carrier passing calls from unknown sources, C is appropriate, but calls will display as unverified to end users.",[20,3597,3599],{"id":3598},"certificate-procurement","Certificate Procurement",[16,3601,3602],{},"You need an STI-SP certificate from an FCC-authorized STI-CA. As of 2025, authorized CAs include:",[2315,3604,3605,3608,3611,3614],{},[788,3606,3607],{},"Comodo\u002FSectigo STI-CA",[788,3609,3610],{},"TransNexus",[788,3612,3613],{},"ATIS STI-CA",[788,3615,3616],{},"Neustar",[16,3618,3619],{},"The certificate is a standard X.509 cert with EC P-256 key and an extension that identifies it as an STI certificate. The CERT URL (where you publish it) must be reachable via HTTPS from any carrier's verification systems.",[48,3621,3623],{"className":50,"code":3622,"language":52,"meta":53,"style":53},"# Generate EC P-256 key pair\nopenssl ecparam -genkey -name prime256v1 -noout -out sti-private.key\nopenssl ec -in sti-private.key -pubout -out sti-public.key\n\n# Create CSR for the STI-CA\nopenssl req -new -key sti-private.key -out sti-request.csr \\\n  -subj \"\u002FC=US\u002FO=Your Company\u002FCN=sti.yourcompany.com\"\n\n# Submit CSR to your chosen STI-CA\n# They issue the certificate and you publish it at a known HTTPS URL:\n# https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\n",[28,3624,3625,3630,3656,3677,3681,3686,3708,3716,3720,3725,3730],{"__ignoreMap":53},[57,3626,3627],{"class":59,"line":60},[57,3628,3629],{"class":63},"# Generate EC P-256 key pair\n",[57,3631,3632,3635,3638,3641,3644,3647,3650,3653],{"class":59,"line":67},[57,3633,3634],{"class":101},"openssl",[57,3636,3637],{"class":74}," ecparam",[57,3639,3640],{"class":70}," -genkey",[57,3642,3643],{"class":70}," -name",[57,3645,3646],{"class":74}," prime256v1",[57,3648,3649],{"class":70}," -noout",[57,3651,3652],{"class":70}," -out",[57,3654,3655],{"class":74}," sti-private.key\n",[57,3657,3658,3660,3663,3666,3669,3672,3674],{"class":59,"line":81},[57,3659,3634],{"class":101},[57,3661,3662],{"class":74}," ec",[57,3664,3665],{"class":70}," -in",[57,3667,3668],{"class":74}," sti-private.key",[57,3670,3671],{"class":70}," -pubout",[57,3673,3652],{"class":70},[57,3675,3676],{"class":74}," sti-public.key\n",[57,3678,3679],{"class":59,"line":91},[57,3680,95],{"emptyLinePlaceholder":94},[57,3682,3683],{"class":59,"line":98},[57,3684,3685],{"class":63},"# Create CSR for the STI-CA\n",[57,3687,3688,3690,3693,3696,3699,3701,3703,3706],{"class":59,"line":123},[57,3689,3634],{"class":101},[57,3691,3692],{"class":74}," req",[57,3694,3695],{"class":70}," -new",[57,3697,3698],{"class":70}," -key",[57,3700,3668],{"class":74},[57,3702,3652],{"class":70},[57,3704,3705],{"class":74}," sti-request.csr",[57,3707,78],{"class":70},[57,3709,3710,3713],{"class":59,"line":132},[57,3711,3712],{"class":70},"  -subj",[57,3714,3715],{"class":74}," \"\u002FC=US\u002FO=Your Company\u002FCN=sti.yourcompany.com\"\n",[57,3717,3718],{"class":59,"line":143},[57,3719,95],{"emptyLinePlaceholder":94},[57,3721,3722],{"class":59,"line":148},[57,3723,3724],{"class":63},"# Submit CSR to your chosen STI-CA\n",[57,3726,3727],{"class":59,"line":154},[57,3728,3729],{"class":63},"# They issue the certificate and you publish it at a known HTTPS URL:\n",[57,3731,3732],{"class":59,"line":175},[57,3733,3734],{"class":63},"# https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\n",[20,3736,3738],{"id":3737},"passport-token-structure","PASSporT Token Structure",[16,3740,3741],{},"A PASSporT (Personal Assertion Token) is a JWT signed with your STI key. The structure:",[48,3743,3747],{"className":3744,"code":3745,"language":3746,"meta":53,"style":53},"language-json shiki shiki-themes github-light github-dark","\u002F\u002F Header\n{\n  \"alg\": \"ES256\",\n  \"typ\": \"passport\",\n  \"ppt\": \"shaken\",\n  \"x5u\": \"https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\"\n}\n\n\u002F\u002F Payload\n{\n  \"attest\": \"A\",\n  \"dest\": {\n    \"tn\": [\"12025551234\"]\n  },\n  \"iat\": 1701388800,\n  \"orig\": {\n    \"tn\": \"14085559876\"\n  },\n  \"origid\": \"550e8400-e29b-41d4-a716-446655440000\"\n}\n","json",[28,3748,3749,3754,3759,3772,3784,3796,3806,3811,3815,3820,3824,3836,3844,3856,3861,3873,3880,3889,3893,3903],{"__ignoreMap":53},[57,3750,3751],{"class":59,"line":60},[57,3752,3753],{"class":63},"\u002F\u002F Header\n",[57,3755,3756],{"class":59,"line":67},[57,3757,3758],{"class":254},"{\n",[57,3760,3761,3764,3766,3769],{"class":59,"line":81},[57,3762,3763],{"class":70},"  \"alg\"",[57,3765,1266],{"class":254},[57,3767,3768],{"class":74},"\"ES256\"",[57,3770,3771],{"class":254},",\n",[57,3773,3774,3777,3779,3782],{"class":59,"line":91},[57,3775,3776],{"class":70},"  \"typ\"",[57,3778,1266],{"class":254},[57,3780,3781],{"class":74},"\"passport\"",[57,3783,3771],{"class":254},[57,3785,3786,3789,3791,3794],{"class":59,"line":98},[57,3787,3788],{"class":70},"  \"ppt\"",[57,3790,1266],{"class":254},[57,3792,3793],{"class":74},"\"shaken\"",[57,3795,3771],{"class":254},[57,3797,3798,3801,3803],{"class":59,"line":123},[57,3799,3800],{"class":70},"  \"x5u\"",[57,3802,1266],{"class":254},[57,3804,3805],{"class":74},"\"https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\"\n",[57,3807,3808],{"class":59,"line":132},[57,3809,3810],{"class":254},"}\n",[57,3812,3813],{"class":59,"line":143},[57,3814,95],{"emptyLinePlaceholder":94},[57,3816,3817],{"class":59,"line":148},[57,3818,3819],{"class":63},"\u002F\u002F Payload\n",[57,3821,3822],{"class":59,"line":154},[57,3823,3758],{"class":254},[57,3825,3826,3829,3831,3834],{"class":59,"line":175},[57,3827,3828],{"class":70},"  \"attest\"",[57,3830,1266],{"class":254},[57,3832,3833],{"class":74},"\"A\"",[57,3835,3771],{"class":254},[57,3837,3838,3841],{"class":59,"line":190},[57,3839,3840],{"class":70},"  \"dest\"",[57,3842,3843],{"class":254},": {\n",[57,3845,3846,3849,3851,3854],{"class":59,"line":195},[57,3847,3848],{"class":70},"    \"tn\"",[57,3850,2432],{"class":254},[57,3852,3853],{"class":74},"\"12025551234\"",[57,3855,2444],{"class":254},[57,3857,3858],{"class":59,"line":207},[57,3859,3860],{"class":254},"  },\n",[57,3862,3863,3866,3868,3871],{"class":59,"line":216},[57,3864,3865],{"class":70},"  \"iat\"",[57,3867,1266],{"class":254},[57,3869,3870],{"class":70},"1701388800",[57,3872,3771],{"class":254},[57,3874,3875,3878],{"class":59,"line":222},[57,3876,3877],{"class":70},"  \"orig\"",[57,3879,3843],{"class":254},[57,3881,3882,3884,3886],{"class":59,"line":490},[57,3883,3848],{"class":70},[57,3885,1266],{"class":254},[57,3887,3888],{"class":74},"\"14085559876\"\n",[57,3890,3891],{"class":59,"line":496},[57,3892,3860],{"class":254},[57,3894,3895,3898,3900],{"class":59,"line":502},[57,3896,3897],{"class":70},"  \"origid\"",[57,3899,1266],{"class":254},[57,3901,3902],{"class":74},"\"550e8400-e29b-41d4-a716-446655440000\"\n",[57,3904,3905],{"class":59,"line":507},[57,3906,3810],{"class":254},[16,3908,3909],{},"Key fields:",[2315,3911,3912,3918,3927,3935,3941],{},[788,3913,3914,3917],{},[28,3915,3916],{},"attest"," — attestation level (A, B, or C)",[788,3919,3920,3923,3924],{},[28,3921,3922],{},"dest.tn"," — destination telephone number in E.164 without the ",[28,3925,3926],{},"+",[788,3928,3929,3932,3933],{},[28,3930,3931],{},"orig.tn"," — originating telephone number in E.164 without the ",[28,3934,3926],{},[788,3936,3937,3940],{},[28,3938,3939],{},"iat"," — Unix timestamp of call origination (must be within 60 seconds of current time)",[788,3942,3943,3946],{},[28,3944,3945],{},"origid"," — unique UUID per call for replay detection",[20,3948,3950],{"id":3949},"generating-the-identity-header","Generating the Identity Header",[48,3952,3954],{"className":1536,"code":3953,"language":1538,"meta":53,"style":53},"import jwt\nimport time\nimport uuid\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.backends import default_backend\n\ndef generate_identity_header(orig_number, dest_number, attestation='A'):\n    # Load private key\n    with open('\u002Fetc\u002Fsti\u002Fprivate.key', 'rb') as f:\n        private_key = serialization.load_pem_private_key(\n            f.read(),\n            password=None,\n            backend=default_backend()\n        )\n    \n    header = {\n        'alg': 'ES256',\n        'typ': 'passport',\n        'ppt': 'shaken',\n        'x5u': 'https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem'\n    }\n    \n    payload = {\n        'attest': attestation,\n        'dest': {'tn': [dest_number.lstrip('+')]},\n        'iat': int(time.time()),\n        'orig': {'tn': orig_number.lstrip('+')},\n        'origid': str(uuid.uuid4())\n    }\n    \n    token = jwt.encode(\n        payload,\n        private_key,\n        algorithm='ES256',\n        headers=header\n    )\n    \n    # SIP Identity header format\n    return f'{token};info=\u003Chttps:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem>;alg=ES256;ppt=\"shaken\"'\n\n# Use in SIP INVITE\nidentity_header = generate_identity_header('+14085559876', '+12025551234', 'A')\n",[28,3955,3956,3961,3965,3970,3975,3980,3984,3989,3994,3999,4004,4009,4014,4019,4024,4028,4033,4038,4043,4048,4053,4058,4062,4067,4072,4077,4082,4087,4092,4096,4100,4105,4110,4115,4120,4125,4130,4134,4139,4144,4148,4153],{"__ignoreMap":53},[57,3957,3958],{"class":59,"line":60},[57,3959,3960],{},"import jwt\n",[57,3962,3963],{"class":59,"line":67},[57,3964,1560],{},[57,3966,3967],{"class":59,"line":81},[57,3968,3969],{},"import uuid\n",[57,3971,3972],{"class":59,"line":91},[57,3973,3974],{},"from cryptography.hazmat.primitives import serialization\n",[57,3976,3977],{"class":59,"line":98},[57,3978,3979],{},"from cryptography.hazmat.backends import default_backend\n",[57,3981,3982],{"class":59,"line":123},[57,3983,95],{"emptyLinePlaceholder":94},[57,3985,3986],{"class":59,"line":132},[57,3987,3988],{},"def generate_identity_header(orig_number, dest_number, attestation='A'):\n",[57,3990,3991],{"class":59,"line":143},[57,3992,3993],{},"    # Load private key\n",[57,3995,3996],{"class":59,"line":148},[57,3997,3998],{},"    with open('\u002Fetc\u002Fsti\u002Fprivate.key', 'rb') as f:\n",[57,4000,4001],{"class":59,"line":154},[57,4002,4003],{},"        private_key = serialization.load_pem_private_key(\n",[57,4005,4006],{"class":59,"line":175},[57,4007,4008],{},"            f.read(),\n",[57,4010,4011],{"class":59,"line":190},[57,4012,4013],{},"            password=None,\n",[57,4015,4016],{"class":59,"line":195},[57,4017,4018],{},"            backend=default_backend()\n",[57,4020,4021],{"class":59,"line":207},[57,4022,4023],{},"        )\n",[57,4025,4026],{"class":59,"line":216},[57,4027,1603],{},[57,4029,4030],{"class":59,"line":222},[57,4031,4032],{},"    header = {\n",[57,4034,4035],{"class":59,"line":490},[57,4036,4037],{},"        'alg': 'ES256',\n",[57,4039,4040],{"class":59,"line":496},[57,4041,4042],{},"        'typ': 'passport',\n",[57,4044,4045],{"class":59,"line":502},[57,4046,4047],{},"        'ppt': 'shaken',\n",[57,4049,4050],{"class":59,"line":507},[57,4051,4052],{},"        'x5u': 'https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem'\n",[57,4054,4055],{"class":59,"line":513},[57,4056,4057],{},"    }\n",[57,4059,4060],{"class":59,"line":519},[57,4061,1603],{},[57,4063,4064],{"class":59,"line":525},[57,4065,4066],{},"    payload = {\n",[57,4068,4069],{"class":59,"line":531},[57,4070,4071],{},"        'attest': attestation,\n",[57,4073,4074],{"class":59,"line":536},[57,4075,4076],{},"        'dest': {'tn': [dest_number.lstrip('+')]},\n",[57,4078,4079],{"class":59,"line":542},[57,4080,4081],{},"        'iat': int(time.time()),\n",[57,4083,4084],{"class":59,"line":548},[57,4085,4086],{},"        'orig': {'tn': orig_number.lstrip('+')},\n",[57,4088,4089],{"class":59,"line":554},[57,4090,4091],{},"        'origid': str(uuid.uuid4())\n",[57,4093,4094],{"class":59,"line":560},[57,4095,4057],{},[57,4097,4098],{"class":59,"line":565},[57,4099,1603],{},[57,4101,4102],{"class":59,"line":571},[57,4103,4104],{},"    token = jwt.encode(\n",[57,4106,4107],{"class":59,"line":577},[57,4108,4109],{},"        payload,\n",[57,4111,4112],{"class":59,"line":582},[57,4113,4114],{},"        private_key,\n",[57,4116,4117],{"class":59,"line":588},[57,4118,4119],{},"        algorithm='ES256',\n",[57,4121,4122],{"class":59,"line":594},[57,4123,4124],{},"        headers=header\n",[57,4126,4127],{"class":59,"line":600},[57,4128,4129],{},"    )\n",[57,4131,4132],{"class":59,"line":605},[57,4133,1603],{},[57,4135,4136],{"class":59,"line":611},[57,4137,4138],{},"    # SIP Identity header format\n",[57,4140,4141],{"class":59,"line":617},[57,4142,4143],{},"    return f'{token};info=\u003Chttps:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem>;alg=ES256;ppt=\"shaken\"'\n",[57,4145,4146],{"class":59,"line":623},[57,4147,95],{"emptyLinePlaceholder":94},[57,4149,4150],{"class":59,"line":2155},[57,4151,4152],{},"# Use in SIP INVITE\n",[57,4154,4155],{"class":59,"line":2165},[57,4156,4157],{},"identity_header = generate_identity_header('+14085559876', '+12025551234', 'A')\n",[20,4159,4161],{"id":4160},"integration-with-kamailio","Integration with Kamailio",[16,4163,4164],{},"Add the Identity header to outbound INVITEs in Kamailio using a Lua script that calls the signing service:",[48,4166,4169],{"className":4167,"code":4168,"language":654},[652],"loadmodule \"app_lua.so\"\nmodparam(\"app_lua\", \"load\", \"\u002Fetc\u002Fkamailio\u002Fstir.lua\")\n\nrequest_route {\n    if (is_method(\"INVITE\") && !has_totag()) {\n        lua_run(\"sign_call\");\n    }\n    t_relay();\n}\n",[28,4170,4168],{"__ignoreMap":53},[48,4172,4176],{"className":4173,"code":4174,"language":4175,"meta":53,"style":53},"language-lua shiki shiki-themes github-light github-dark","-- \u002Fetc\u002Fkamailio\u002Fstir.lua\nfunction sign_call()\n    local orig = KSR.pv.get(\"$fU\")\n    local dest = KSR.pv.get(\"$rU\")\n    \n    -- Call local signing microservice\n    local http = require(\"socket.http\")\n    local json = require(\"cjson\")\n    \n    local body = json.encode({orig = orig, dest = dest, attest = \"A\"})\n    local response, status = http.request(\n        \"http:\u002F\u002F127.0.0.1:8080\u002Fsign\",\n        body\n    )\n    \n    if status == 200 then\n        local result = json.decode(response)\n        KSR.hdr.append(\"Identity: \" .. result.identity_header .. \"\\r\\n\")\n    else\n        KSR.log(\"err\", \"STIR signing failed: \" .. tostring(status))\n    end\nend\n","lua",[28,4177,4178,4183,4188,4193,4198,4202,4207,4212,4217,4221,4226,4231,4236,4241,4245,4249,4254,4259,4264,4269,4274,4279],{"__ignoreMap":53},[57,4179,4180],{"class":59,"line":60},[57,4181,4182],{},"-- \u002Fetc\u002Fkamailio\u002Fstir.lua\n",[57,4184,4185],{"class":59,"line":67},[57,4186,4187],{},"function sign_call()\n",[57,4189,4190],{"class":59,"line":81},[57,4191,4192],{},"    local orig = KSR.pv.get(\"$fU\")\n",[57,4194,4195],{"class":59,"line":91},[57,4196,4197],{},"    local dest = KSR.pv.get(\"$rU\")\n",[57,4199,4200],{"class":59,"line":98},[57,4201,1603],{},[57,4203,4204],{"class":59,"line":123},[57,4205,4206],{},"    -- Call local signing microservice\n",[57,4208,4209],{"class":59,"line":132},[57,4210,4211],{},"    local http = require(\"socket.http\")\n",[57,4213,4214],{"class":59,"line":143},[57,4215,4216],{},"    local json = require(\"cjson\")\n",[57,4218,4219],{"class":59,"line":148},[57,4220,1603],{},[57,4222,4223],{"class":59,"line":154},[57,4224,4225],{},"    local body = json.encode({orig = orig, dest = dest, attest = \"A\"})\n",[57,4227,4228],{"class":59,"line":175},[57,4229,4230],{},"    local response, status = http.request(\n",[57,4232,4233],{"class":59,"line":190},[57,4234,4235],{},"        \"http:\u002F\u002F127.0.0.1:8080\u002Fsign\",\n",[57,4237,4238],{"class":59,"line":195},[57,4239,4240],{},"        body\n",[57,4242,4243],{"class":59,"line":207},[57,4244,4129],{},[57,4246,4247],{"class":59,"line":216},[57,4248,1603],{},[57,4250,4251],{"class":59,"line":222},[57,4252,4253],{},"    if status == 200 then\n",[57,4255,4256],{"class":59,"line":490},[57,4257,4258],{},"        local result = json.decode(response)\n",[57,4260,4261],{"class":59,"line":496},[57,4262,4263],{},"        KSR.hdr.append(\"Identity: \" .. result.identity_header .. \"\\r\\n\")\n",[57,4265,4266],{"class":59,"line":502},[57,4267,4268],{},"    else\n",[57,4270,4271],{"class":59,"line":507},[57,4272,4273],{},"        KSR.log(\"err\", \"STIR signing failed: \" .. tostring(status))\n",[57,4275,4276],{"class":59,"line":513},[57,4277,4278],{},"    end\n",[57,4280,4281],{"class":59,"line":519},[57,4282,4283],{},"end\n",[16,4285,4286],{},"Run the signing microservice as a local sidecar process. Keep it on localhost to avoid network latency on the signing path — adding 100ms to call setup time for every INVITE is unacceptable at scale.",[20,4288,4290],{"id":4289},"verification-on-the-terminating-side","Verification on the Terminating Side",[16,4292,4293],{},"If you also receive calls from other carriers, verify incoming Identity headers:",[48,4295,4297],{"className":1536,"code":4296,"language":1538,"meta":53,"style":53},"import jwt\nimport requests\nimport time\nfrom cryptography.x509 import load_pem_x509_certificate\n\ndef verify_identity_header(identity_header, orig_number, dest_number):\n    # Parse the header: token;info=\u003Ccert_url>;alg=ES256;ppt=\"shaken\"\n    parts = identity_header.split(';')\n    token = parts[0].strip()\n    cert_url = parts[1].replace('info=\u003C', '').replace('>', '').strip()\n    \n    # Fetch the signing certificate\n    cert_response = requests.get(cert_url, timeout=2)\n    cert = load_pem_x509_certificate(cert_response.content)\n    public_key = cert.public_key()\n    \n    try:\n        payload = jwt.decode(\n            token,\n            public_key,\n            algorithms=['ES256'],\n            options={'verify_exp': False}\n        )\n    except jwt.InvalidSignatureError:\n        return {'valid': False, 'reason': 'Invalid signature'}\n    \n    # Validate payload claims\n    if abs(time.time() - payload['iat']) > 60:\n        return {'valid': False, 'reason': 'Token expired'}\n    \n    if payload['orig']['tn'] != orig_number.lstrip('+'):\n        return {'valid': False, 'reason': 'Orig number mismatch'}\n    \n    return {\n        'valid': True,\n        'attestation': payload['attest'],\n        'origid': payload['origid']\n    }\n",[28,4298,4299,4303,4308,4312,4317,4321,4326,4331,4336,4341,4346,4350,4355,4360,4365,4370,4374,4379,4384,4389,4394,4399,4404,4408,4413,4418,4422,4427,4432,4437,4441,4446,4451,4455,4460,4465,4470,4475],{"__ignoreMap":53},[57,4300,4301],{"class":59,"line":60},[57,4302,3960],{},[57,4304,4305],{"class":59,"line":67},[57,4306,4307],{},"import requests\n",[57,4309,4310],{"class":59,"line":81},[57,4311,1560],{},[57,4313,4314],{"class":59,"line":91},[57,4315,4316],{},"from cryptography.x509 import load_pem_x509_certificate\n",[57,4318,4319],{"class":59,"line":98},[57,4320,95],{"emptyLinePlaceholder":94},[57,4322,4323],{"class":59,"line":123},[57,4324,4325],{},"def verify_identity_header(identity_header, orig_number, dest_number):\n",[57,4327,4328],{"class":59,"line":132},[57,4329,4330],{},"    # Parse the header: token;info=\u003Ccert_url>;alg=ES256;ppt=\"shaken\"\n",[57,4332,4333],{"class":59,"line":143},[57,4334,4335],{},"    parts = identity_header.split(';')\n",[57,4337,4338],{"class":59,"line":148},[57,4339,4340],{},"    token = parts[0].strip()\n",[57,4342,4343],{"class":59,"line":154},[57,4344,4345],{},"    cert_url = parts[1].replace('info=\u003C', '').replace('>', '').strip()\n",[57,4347,4348],{"class":59,"line":175},[57,4349,1603],{},[57,4351,4352],{"class":59,"line":190},[57,4353,4354],{},"    # Fetch the signing certificate\n",[57,4356,4357],{"class":59,"line":195},[57,4358,4359],{},"    cert_response = requests.get(cert_url, timeout=2)\n",[57,4361,4362],{"class":59,"line":207},[57,4363,4364],{},"    cert = load_pem_x509_certificate(cert_response.content)\n",[57,4366,4367],{"class":59,"line":216},[57,4368,4369],{},"    public_key = cert.public_key()\n",[57,4371,4372],{"class":59,"line":222},[57,4373,1603],{},[57,4375,4376],{"class":59,"line":490},[57,4377,4378],{},"    try:\n",[57,4380,4381],{"class":59,"line":496},[57,4382,4383],{},"        payload = jwt.decode(\n",[57,4385,4386],{"class":59,"line":502},[57,4387,4388],{},"            token,\n",[57,4390,4391],{"class":59,"line":507},[57,4392,4393],{},"            public_key,\n",[57,4395,4396],{"class":59,"line":513},[57,4397,4398],{},"            algorithms=['ES256'],\n",[57,4400,4401],{"class":59,"line":519},[57,4402,4403],{},"            options={'verify_exp': False}\n",[57,4405,4406],{"class":59,"line":525},[57,4407,4023],{},[57,4409,4410],{"class":59,"line":531},[57,4411,4412],{},"    except jwt.InvalidSignatureError:\n",[57,4414,4415],{"class":59,"line":536},[57,4416,4417],{},"        return {'valid': False, 'reason': 'Invalid signature'}\n",[57,4419,4420],{"class":59,"line":542},[57,4421,1603],{},[57,4423,4424],{"class":59,"line":548},[57,4425,4426],{},"    # Validate payload claims\n",[57,4428,4429],{"class":59,"line":554},[57,4430,4431],{},"    if abs(time.time() - payload['iat']) > 60:\n",[57,4433,4434],{"class":59,"line":560},[57,4435,4436],{},"        return {'valid': False, 'reason': 'Token expired'}\n",[57,4438,4439],{"class":59,"line":565},[57,4440,1603],{},[57,4442,4443],{"class":59,"line":571},[57,4444,4445],{},"    if payload['orig']['tn'] != orig_number.lstrip('+'):\n",[57,4447,4448],{"class":59,"line":577},[57,4449,4450],{},"        return {'valid': False, 'reason': 'Orig number mismatch'}\n",[57,4452,4453],{"class":59,"line":582},[57,4454,1603],{},[57,4456,4457],{"class":59,"line":588},[57,4458,4459],{},"    return {\n",[57,4461,4462],{"class":59,"line":594},[57,4463,4464],{},"        'valid': True,\n",[57,4466,4467],{"class":59,"line":600},[57,4468,4469],{},"        'attestation': payload['attest'],\n",[57,4471,4472],{"class":59,"line":605},[57,4473,4474],{},"        'origid': payload['origid']\n",[57,4476,4477],{"class":59,"line":611},[57,4478,4057],{},[16,4480,4481],{},"Cache certificate fetches with a 1-hour TTL. The STI-CA certificate changes rarely, and fetching it on every call adds latency and creates a dependency on an external HTTPS endpoint in the call path.",[20,4483,4485],{"id":4484},"fcc-compliance-timeline","FCC Compliance Timeline",[661,4487,4488,4501],{},[664,4489,4490],{},[667,4491,4492,4495,4498],{},[670,4493,4494],{},"Requirement",[670,4496,4497],{},"Deadline",[670,4499,4500],{},"Who",[677,4502,4503,4514,4525,4536],{},[667,4504,4505,4508,4511],{},[682,4506,4507],{},"Implement STIR\u002FSHAKEN",[682,4509,4510],{},"June 2021",[682,4512,4513],{},"Major voice providers",[667,4515,4516,4519,4522],{},[682,4517,4518],{},"STIR\u002FSHAKEN or robocall mitigation",[682,4520,4521],{},"June 2022",[682,4523,4524],{},"Small voice providers",[667,4526,4527,4530,4533],{},[682,4528,4529],{},"Certificate must be from authorized STI-CA",[682,4531,4532],{},"Ongoing",[682,4534,4535],{},"All signing providers",[667,4537,4538,4541,4544],{},[682,4539,4540],{},"Annual certification filing",[682,4542,4543],{},"Annually",[682,4545,4546],{},"All providers",[16,4548,4549],{},"File your annual certification at the FCC's Robocall Mitigation Database. Failure to file results in downstream carriers refusing to accept your traffic — a business-ending consequence for a SIP trunking provider.",[1009,4551,4552],{},"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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":53,"searchDepth":67,"depth":67,"links":4554},[4555,4556,4557,4558,4559,4560,4561,4562],{"id":3525,"depth":67,"text":3526},{"id":3538,"depth":67,"text":3539},{"id":3598,"depth":67,"text":3599},{"id":3737,"depth":67,"text":3738},{"id":3949,"depth":67,"text":3950},{"id":4160,"depth":67,"text":4161},{"id":4289,"depth":67,"text":4290},{"id":4484,"depth":67,"text":4485},"SIP","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1487058792275-0ad4aaf24ca7?w=1200&q=80","2025-12-01","Implement STIR\u002FSHAKEN caller ID authentication for SIP trunking: PASSporT token generation, attestation levels, STI-SP certificate management, and SHAKEN verification flow.",{},"\u002Fblog\u002Fsip-trunking-stir-shaken",{"title":3511,"description":4566},"blog\u002Fsip-trunking-stir-shaken",[4572,4573,4574,4575,4576,4577],"stir-shaken","sip-trunking","caller-id","robocall","compliance","jwt","EsDNmAwSo7VBbFj6VNn5alnkHRUGBYzzxrfM-PXsTvs",{"id":4580,"title":4581,"author":7,"body":4582,"category":4563,"coverImage":1024,"date":4953,"description":4954,"extension":1027,"meta":4955,"navigation":94,"path":4956,"readingTime":143,"seo":4957,"stem":4958,"tags":4959,"__hash__":4963},"posts\u002Fblog\u002Fkamailio-dispatcher-failover.md","Building resilient SIP routing with Kamailio dispatcher",{"type":9,"value":4583,"toc":4944},[4584,4587,4590,4593,4597,4600,4606,4646,4649,4662,4666,4669,4675,4707,4711,4718,4724,4730,4734,4737,4743,4752,4756,4762,4823,4829,4833,4836,4872,4875,4905,4908,4912,4915,4938,4941],[12,4585,4581],{"id":4586},"building-resilient-sip-routing-with-kamailio-dispatcher",[16,4588,4589],{},"Most Kamailio deployments that start with a round-robin dispatcher don't stay round-robin for long. When a carrier route degrades — increasing packet loss, rising PDD, occasional 5xx storms — you need Kamailio to detect it and route around it without human intervention. That's what the dispatcher module with active probing gives you.",[16,4591,4592],{},"This post covers the configuration in enough detail to ship a production setup, not just a working demo.",[20,4594,4596],{"id":4595},"dispatcherlist-the-basics","dispatcher.list: the basics",[16,4598,4599],{},"The dispatcher module reads destinations from a file or database. The file format is one destination per line:",[48,4601,4604],{"className":4602,"code":4603,"language":654},[652],"# \u002Fetc\u002Fkamailio\u002Fdispatcher.list\n# setid  destination            flags  priority\n1        sip:carrier-a.example:5060   0        10\n1        sip:carrier-b.example:5060   0        10\n2        sip:backup.example:5060      0        5\n",[28,4605,4603],{"__ignoreMap":53},[2315,4607,4608,4618,4640],{},[788,4609,4610,4613,4614,4617],{},[2311,4611,4612],{},"setid"," groups destinations. ",[28,4615,4616],{},"ds_select_dst(1, 4)"," picks from setid 1 using algorithm 4 (round-robin with weights).",[788,4619,4620,4623,4624,4627,4628,4631,4632,4635,4636,4639],{},[2311,4621,4622],{},"flags"," mark destination state. ",[28,4625,4626],{},"0"," = active, ",[28,4629,4630],{},"1"," = inactive, ",[28,4633,4634],{},"2"," = probing. Kamailio updates these at runtime via ",[28,4637,4638],{},"ds_set_state()",".",[788,4641,4642,4645],{},[2311,4643,4644],{},"priority"," breaks ties in weight-based algorithms. Higher priority wins when weights are equal.",[16,4647,4648],{},"Reload the list without restart:",[48,4650,4652],{"className":50,"code":4651,"language":52,"meta":53,"style":53},"kamcmd dispatcher.reload\n",[28,4653,4654],{"__ignoreMap":53},[57,4655,4656,4659],{"class":59,"line":60},[57,4657,4658],{"class":101},"kamcmd",[57,4660,4661],{"class":74}," dispatcher.reload\n",[20,4663,4665],{"id":4664},"probing-modes","Probing modes",[16,4667,4668],{},"Active probing sends SIP OPTIONS to each destination on a configurable interval. When a destination stops responding, Kamailio marks it inactive and stops routing to it.",[48,4670,4673],{"className":4671,"code":4672,"language":654},[652],"# kamailio.cfg\nmodparam(\"dispatcher\", \"ds_probing_mode\", 1)\nmodparam(\"dispatcher\", \"ds_ping_interval\", 10)\nmodparam(\"dispatcher\", \"ds_probing_threshold\", 3)\nmodparam(\"dispatcher\", \"ds_inactive_threshold\", 3)\n",[28,4674,4672],{"__ignoreMap":53},[2315,4676,4677,4689,4695,4701],{},[788,4678,4679,4682,4683,4685,4686,4688],{},[28,4680,4681],{},"ds_probing_mode = 1"," — probe all active and inactive destinations. Use ",[28,4684,4634],{}," to probe only destinations flagged for probing (flag ",[28,4687,4634],{}," in dispatcher.list).",[788,4690,4691,4694],{},[28,4692,4693],{},"ds_ping_interval"," — seconds between OPTIONS sends. 10s is reasonable for carrier routes; go lower (5s) for critical paths, higher (30s) for backup destinations.",[788,4696,4697,4700],{},[28,4698,4699],{},"ds_probing_threshold"," — consecutive failed probes before marking destination inactive.",[788,4702,4703,4706],{},[28,4704,4705],{},"ds_inactive_threshold"," — consecutive successful probes before marking an inactive destination active again.",[20,4708,4710],{"id":4709},"handling-probe-responses-in-kamailiocfg","Handling probe responses in kamailio.cfg",[16,4712,4713,4714,4717],{},"The dispatcher module needs an ",[28,4715,4716],{},"onreply_route"," to process OPTIONS 200 OK responses:",[48,4719,4722],{"className":4720,"code":4721,"language":654},[652],"onreply_route[MANAGE_REPLY] {\n    if (status =~ \"[12][0-9][0-9]\") {\n        ds_mark_dst(\"a\"); # mark as active\n    }\n}\n\nfailure_route[MANAGE_FAILURE] {\n    if (t_is_canceled()) {\n        exit;\n    }\n    ds_mark_dst(\"i\"); # mark as inactive\n    if (!ds_next_dst()) {\n        # no more destinations — send 503\n        send_reply(\"503\", \"Service Unavailable\");\n        exit;\n    }\n    t_relay();\n}\n",[28,4723,4721],{"__ignoreMap":53},[16,4725,4726,4729],{},[28,4727,4728],{},"ds_next_dst()"," moves to the next destination in the set and returns false when the set is exhausted. This gives you sequential failover within a single INVITE attempt.",[20,4731,4733],{"id":4732},"failover-priority-ordering","Failover priority ordering",[16,4735,4736],{},"When you have a primary carrier and a backup, use separate setids with explicit fallback logic:",[48,4738,4741],{"className":4739,"code":4740,"language":654},[652],"route[CARRIER_ROUTE] {\n    # Try setid 1 (primary carriers) first\n    if (!ds_select_dst(1, 4)) {\n        # setid 1 completely inactive — fall to backup\n        if (!ds_select_dst(2, 0)) {\n            send_reply(\"503\", \"No route available\");\n            exit;\n        }\n    }\n    route(RELAY);\n}\n",[28,4742,4740],{"__ignoreMap":53},[16,4744,4745,4746,4748,4749,4751],{},"Set ",[28,4747,4634],{}," (algorithm ",[28,4750,4626],{}," = hash over call-id) acts as a stable backup. This setup means: try the primary pool first, and only if every destination in it is probing-inactive, fall through to the backup.",[20,4753,4755],{"id":4754},"keepalive-tuning","Keepalive tuning",[16,4757,4758,4759,4761],{},"The right ",[28,4760,4693],{}," depends on your NAT\u002Ffirewall environment and your SLA:",[661,4763,4764,4777],{},[664,4765,4766],{},[667,4767,4768,4771,4774],{},[670,4769,4770],{},"Environment",[670,4772,4773],{},"Recommended interval",[670,4775,4776],{},"Reasoning",[677,4778,4779,4790,4801,4812],{},[667,4780,4781,4784,4787],{},[682,4782,4783],{},"Carrier interconnect (no NAT)",[682,4785,4786],{},"30s",[682,4788,4789],{},"Low keepalive cost, carrier routes are stable",[667,4791,4792,4795,4798],{},[682,4793,4794],{},"Enterprise trunks (behind FW)",[682,4796,4797],{},"10s",[682,4799,4800],{},"Firewall state tables time out in 30-60s",[667,4802,4803,4806,4809],{},[682,4804,4805],{},"WebRTC gateway",[682,4807,4808],{},"5s",[682,4810,4811],{},"Fast detection matters for UX",[667,4813,4814,4817,4820],{},[682,4815,4816],{},"Backup\u002Ffailover only",[682,4818,4819],{},"60s",[682,4821,4822],{},"Just need to know it's alive",[16,4824,4825,4826,4828],{},"Don't set ",[28,4827,4693],{}," below 5s unless you have a very specific reason — you'll saturate the OPTIONS handling on busy carrier SBCs.",[20,4830,4832],{"id":4831},"monitoring-dispatcher-state","Monitoring dispatcher state",[16,4834,4835],{},"Check current state via the Kamailio management interface:",[48,4837,4839],{"className":50,"code":4838,"language":52,"meta":53,"style":53},"# All destinations in all sets\nkamcmd dispatcher.list\n\n# Specific set\nkamcmd dispatcher.list 1\n",[28,4840,4841,4846,4853,4857,4862],{"__ignoreMap":53},[57,4842,4843],{"class":59,"line":60},[57,4844,4845],{"class":63},"# All destinations in all sets\n",[57,4847,4848,4850],{"class":59,"line":67},[57,4849,4658],{"class":101},[57,4851,4852],{"class":74}," dispatcher.list\n",[57,4854,4855],{"class":59,"line":81},[57,4856,95],{"emptyLinePlaceholder":94},[57,4858,4859],{"class":59,"line":91},[57,4860,4861],{"class":63},"# Specific set\n",[57,4863,4864,4866,4869],{"class":59,"line":98},[57,4865,4658],{"class":101},[57,4867,4868],{"class":74}," dispatcher.list",[57,4870,4871],{"class":70}," 1\n",[16,4873,4874],{},"Output shows each destination with its current flags. Automate this into your monitoring stack:",[48,4876,4878],{"className":50,"code":4877,"language":52,"meta":53,"style":53},"# Prometheus textfile exporter (simplified)\nkamcmd dispatcher.list | awk '\u002F^{\u002F { next } \u002FURI:\u002F { uri=$2 } \u002FFlags:\u002F { flags=$2 } \u002FLatency:\u002F { print \"kamailio_dispatcher_latency{uri=\\\"\" uri \"\\\"} \" $2 }' > \u002Fvar\u002Flib\u002Fnode_exporter\u002Fkamailio_dispatcher.prom\n",[28,4879,4880,4885],{"__ignoreMap":53},[57,4881,4882],{"class":59,"line":60},[57,4883,4884],{"class":63},"# Prometheus textfile exporter (simplified)\n",[57,4886,4887,4889,4891,4893,4896,4899,4902],{"class":59,"line":67},[57,4888,4658],{"class":101},[57,4890,4868],{"class":74},[57,4892,362],{"class":84},[57,4894,4895],{"class":101}," awk",[57,4897,4898],{"class":74}," '\u002F^{\u002F { next } \u002FURI:\u002F { uri=$2 } \u002FFlags:\u002F { flags=$2 } \u002FLatency:\u002F { print \"kamailio_dispatcher_latency{uri=\\\"\" uri \"\\\"} \" $2 }'",[57,4900,4901],{"class":84}," >",[57,4903,4904],{"class":74}," \u002Fvar\u002Flib\u002Fnode_exporter\u002Fkamailio_dispatcher.prom\n",[16,4906,4907],{},"With this in place, Grafana can alert when a destination's latency climbs above threshold before it drops completely — giving you early warning rather than reactive failover.",[20,4909,4911],{"id":4910},"putting-it-together","Putting it together",[16,4913,4914],{},"A production-ready dispatcher setup has:",[785,4916,4917,4920,4923,4932,4935],{},[788,4918,4919],{},"dispatcher.list with setids, flags, and explicit priorities",[788,4921,4922],{},"Active probing with tuned threshold values",[788,4924,4925,4928,4929,4931],{},[28,4926,4927],{},"failure_route"," that calls ",[28,4930,4728],{}," for sequential failover",[788,4933,4934],{},"Separate setids for primary and backup pools",[788,4936,4937],{},"Monitoring that surfaces per-destination state and latency",[16,4939,4940],{},"The dispatcher module is one of Kamailio's most reliable components — we've seen deployments handle 50k+ concurrent sessions with dispatcher failover completing in under 2 seconds on a single-node failure. The key is tuning the probing parameters to match your actual carrier SLA and your tolerance for misdirected calls during the detection window.",[1009,4942,4943],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":53,"searchDepth":67,"depth":67,"links":4945},[4946,4947,4948,4949,4950,4951,4952],{"id":4595,"depth":67,"text":4596},{"id":4664,"depth":67,"text":4665},{"id":4709,"depth":67,"text":4710},{"id":4732,"depth":67,"text":4733},{"id":4754,"depth":67,"text":4755},{"id":4831,"depth":67,"text":4832},{"id":4910,"depth":67,"text":4911},"2025-11-15","How to configure Kamailio's dispatcher module for production-grade failover: dispatcher.list syntax, probing modes, failover priorities, and keepalive tuning.",{},"\u002Fblog\u002Fkamailio-dispatcher-failover",{"title":4581,"description":4954},"blog\u002Fkamailio-dispatcher-failover",[1036,4960,4961,4962],"dispatcher","failover","sip-routing","-l95leN4s_ekVVpHoAppgfpVCoUM5TlYHXmg4uOMmWY",{"id":4965,"title":4966,"author":7,"body":4967,"category":5804,"coverImage":5805,"date":5806,"description":5807,"extension":1027,"meta":5808,"navigation":94,"path":5809,"readingTime":148,"seo":5810,"stem":5811,"tags":5812,"__hash__":5818},"posts\u002Fblog\u002Fvicidial-predictive-dialer-tuning.md","Vicidial Predictive Dialer Tuning: Maximizing Agent Productivity",{"type":9,"value":4968,"toc":5794},[4969,4972,4975,4979,4982,4988,4991,5005,5011,5014,5018,5021,5161,5176,5180,5187,5195,5201,5256,5259,5270,5277,5354,5358,5365,5368,5426,5429,5484,5490,5494,5497,5602,5605,5609,5612,5689,5692,5696,5699,5713,5716,5720,5789,5792],[12,4970,4966],{"id":4971},"vicidial-predictive-dialer-tuning-maximizing-agent-productivity",[16,4973,4974],{},"Vicidial is the most widely deployed open-source predictive dialer. A default installation works. A tuned installation delivers 45–55 minutes of talk time per agent hour instead of the 30–35 minutes most teams see out of the box. The difference comes down to dial ratio math, database query performance, and understanding how Vicidial's adaptive algorithm responds to your specific lead list and agent pool characteristics.",[20,4976,4978],{"id":4977},"how-the-predictive-algorithm-works","How the Predictive Algorithm Works",[16,4980,4981],{},"Vicidial's predictive dialer uses an adaptive algorithm that continuously adjusts the dial ratio (calls dialed per available agent) based on recent outcome statistics. The core formula:",[48,4983,4986],{"className":4984,"code":4985,"language":654},[652],"dial_ratio = available_agents × (1 \u002F (1 - target_abandon_rate)) × safety_margin\n",[28,4987,4985],{"__ignoreMap":53},[16,4989,4990],{},"Where:",[2315,4992,4993,4999],{},[788,4994,4995,4998],{},[28,4996,4997],{},"target_abandon_rate"," is typically 3% (FTC limit in the US) or 5% (Ofcom limit in the UK)",[788,5000,5001,5004],{},[28,5002,5003],{},"safety_margin"," is 0.85–0.95 (accounts for prediction error)",[16,5006,5007,5008],{},"At 3% target abandon rate: ",[28,5009,5010],{},"dial_ratio = agents × 1.031 × 0.9 ≈ agents × 0.928",[16,5012,5013],{},"This means with 10 available agents, Vicidial should be dialing roughly 9–10 simultaneous calls. Many installations are tuned too conservatively (ratio 1.2:1) or too aggressively (ratio 3:1), both of which hurt productivity.",[20,5015,5017],{"id":5016},"key-vicidial-campaign-settings","Key Vicidial Campaign Settings",[16,5019,5020],{},"Navigate to Admin > Campaigns > Edit Campaign for each campaign you want to tune:",[661,5022,5023,5038],{},[664,5024,5025],{},[667,5026,5027,5030,5033,5036],{},[670,5028,5029],{},"Setting",[670,5031,5032],{},"Default",[670,5034,5035],{},"Recommended",[670,5037,675],{},[677,5039,5040,5056,5072,5087,5103,5117,5131,5147],{},[667,5041,5042,5047,5050,5053],{},[682,5043,5044],{},[28,5045,5046],{},"dial_ratio",[682,5048,5049],{},"1.0",[682,5051,5052],{},"Adaptive",[682,5054,5055],{},"Calls per available agent",[667,5057,5058,5063,5066,5069],{},[682,5059,5060],{},[28,5061,5062],{},"auto_dial_level",[682,5064,5065],{},"MANUAL",[682,5067,5068],{},"ADAPT_TANH",[682,5070,5071],{},"Use adaptive algorithm",[667,5073,5074,5079,5082,5084],{},[682,5075,5076],{},[28,5077,5078],{},"adaptive_dropped_percentage",[682,5080,5081],{},"3",[682,5083,5081],{},[682,5085,5086],{},"FTC compliance target",[667,5088,5089,5094,5097,5100],{},[682,5090,5091],{},[28,5092,5093],{},"dial_timeout",[682,5095,5096],{},"30",[682,5098,5099],{},"20–25",[682,5101,5102],{},"Seconds before AMD \u002F abandon",[667,5104,5105,5110,5112,5114],{},[682,5106,5107],{},[28,5108,5109],{},"drop_call_seconds",[682,5111,5081],{},[682,5113,4634],{},[682,5115,5116],{},"Seconds before drop if no agent",[667,5118,5119,5124,5126,5128],{},[682,5120,5121],{},[28,5122,5123],{},"dead_seconds",[682,5125,4630],{},[682,5127,4626],{},[682,5129,5130],{},"Delay before drop (set 0)",[667,5132,5133,5138,5141,5144],{},[682,5134,5135],{},[28,5136,5137],{},"available_only_ratio",[682,5139,5140],{},"N",[682,5142,5143],{},"Y",[682,5145,5146],{},"Only count truly available agents",[667,5148,5149,5154,5156,5158],{},[682,5150,5151],{},[28,5152,5153],{},"calls_waiting_size_limit",[682,5155,4626],{},[682,5157,4634],{},[682,5159,5160],{},"Calls waiting buffer",[16,5162,5163,5164,5167,5168,5171,5172,5175],{},"Setting ",[28,5165,5166],{},"auto_dial_level=ADAPT_TANH"," activates Vicidial's built-in adaptive mode. The ",[28,5169,5170],{},"TANH"," variant uses a hyperbolic tangent function to smooth ratio adjustments, preventing the oscillation you see with ",[28,5173,5174],{},"ADAPT_AVERAGE"," when agent availability spikes or drops suddenly.",[20,5177,5179],{"id":5178},"answering-machine-detection-amd-tuning","Answering Machine Detection (AMD) Tuning",[16,5181,5182,5183,5186],{},"AMD is the biggest lever for talk-time productivity. Vicidial uses Asterisk's ",[28,5184,5185],{},"AMD"," application, which analyzes audio energy and silence patterns to classify calls as human or machine. Poor AMD configuration causes two problems:",[2315,5188,5189,5192],{},[788,5190,5191],{},"False positives (humans classified as machines) — agents never hear the call",[788,5193,5194],{},"False negatives (machines passed to agents) — agents hear voicemail, waste time",[16,5196,5197,5198,1717],{},"Tune these parameters in ",[28,5199,5200],{},"\u002Fetc\u002Fasterisk\u002Famd.conf",[48,5202,5204],{"className":406,"code":5203,"language":408,"meta":53,"style":53},"[general]\ninitial_silence = 2500      ; Max silence before first word (ms)\ngreeting = 1500             ; Max length of greeting (ms)\nafter_greeting_silence = 800 ; Silence after greeting = machine\ntotal_analysis_time = 5000  ; Max time to analyze\nmin_word_length = 100       ; Min word length (ms) — below this is noise\nbetween_words_silence = 50  ; Silence between words = word boundary\nmaximum_number_of_words = 3 ; If > 3 words, probably machine\nsilence_threshold = 256     ; Energy threshold for silence detection\nmaximum_word_length = 5000  ; Single word longer than this = machine\n",[28,5205,5206,5211,5216,5221,5226,5231,5236,5241,5246,5251],{"__ignoreMap":53},[57,5207,5208],{"class":59,"line":60},[57,5209,5210],{},"[general]\n",[57,5212,5213],{"class":59,"line":67},[57,5214,5215],{},"initial_silence = 2500      ; Max silence before first word (ms)\n",[57,5217,5218],{"class":59,"line":81},[57,5219,5220],{},"greeting = 1500             ; Max length of greeting (ms)\n",[57,5222,5223],{"class":59,"line":91},[57,5224,5225],{},"after_greeting_silence = 800 ; Silence after greeting = machine\n",[57,5227,5228],{"class":59,"line":98},[57,5229,5230],{},"total_analysis_time = 5000  ; Max time to analyze\n",[57,5232,5233],{"class":59,"line":123},[57,5234,5235],{},"min_word_length = 100       ; Min word length (ms) — below this is noise\n",[57,5237,5238],{"class":59,"line":132},[57,5239,5240],{},"between_words_silence = 50  ; Silence between words = word boundary\n",[57,5242,5243],{"class":59,"line":143},[57,5244,5245],{},"maximum_number_of_words = 3 ; If > 3 words, probably machine\n",[57,5247,5248],{"class":59,"line":148},[57,5249,5250],{},"silence_threshold = 256     ; Energy threshold for silence detection\n",[57,5252,5253],{"class":59,"line":154},[57,5254,5255],{},"maximum_word_length = 5000  ; Single word longer than this = machine\n",[16,5257,5258],{},"Measure your AMD accuracy weekly. In a well-tuned system:",[2315,5260,5261,5264,5267],{},[788,5262,5263],{},"Human detection rate: >90%",[788,5265,5266],{},"Machine detection rate: >70%",[788,5268,5269],{},"False positive rate (human called machine): \u003C5%",[16,5271,5272,5273,5276],{},"Log AMD results via Vicidial's ",[28,5274,5275],{},"call_log"," table and query accuracy:",[48,5278,5282],{"className":5279,"code":5280,"language":5281,"meta":53,"style":53},"language-sql shiki shiki-themes github-light github-dark","SELECT\n    call_date::date AS day,\n    SUM(CASE WHEN status = 'AMD' THEN 1 ELSE 0 END) AS machine_detected,\n    SUM(CASE WHEN status IN ('SALE', 'NI', 'NA', 'DNC') THEN 1 ELSE 0 END) AS human_connected,\n    SUM(CASE WHEN status = 'DROP' THEN 1 ELSE 0 END) AS dropped,\n    COUNT(*) AS total_calls,\n    ROUND(\n        100.0 * SUM(CASE WHEN status IN ('SALE','NI','NA','DNC') THEN 1 ELSE 0 END) \u002F COUNT(*),\n        1\n    ) AS connect_rate_pct\nFROM vicidial_log\nWHERE call_date > NOW() - INTERVAL '7 days'\nGROUP BY call_date::date\nORDER BY day DESC;\n","sql",[28,5283,5284,5289,5294,5299,5304,5309,5314,5319,5324,5329,5334,5339,5344,5349],{"__ignoreMap":53},[57,5285,5286],{"class":59,"line":60},[57,5287,5288],{},"SELECT\n",[57,5290,5291],{"class":59,"line":67},[57,5292,5293],{},"    call_date::date AS day,\n",[57,5295,5296],{"class":59,"line":81},[57,5297,5298],{},"    SUM(CASE WHEN status = 'AMD' THEN 1 ELSE 0 END) AS machine_detected,\n",[57,5300,5301],{"class":59,"line":91},[57,5302,5303],{},"    SUM(CASE WHEN status IN ('SALE', 'NI', 'NA', 'DNC') THEN 1 ELSE 0 END) AS human_connected,\n",[57,5305,5306],{"class":59,"line":98},[57,5307,5308],{},"    SUM(CASE WHEN status = 'DROP' THEN 1 ELSE 0 END) AS dropped,\n",[57,5310,5311],{"class":59,"line":123},[57,5312,5313],{},"    COUNT(*) AS total_calls,\n",[57,5315,5316],{"class":59,"line":132},[57,5317,5318],{},"    ROUND(\n",[57,5320,5321],{"class":59,"line":143},[57,5322,5323],{},"        100.0 * SUM(CASE WHEN status IN ('SALE','NI','NA','DNC') THEN 1 ELSE 0 END) \u002F COUNT(*),\n",[57,5325,5326],{"class":59,"line":148},[57,5327,5328],{},"        1\n",[57,5330,5331],{"class":59,"line":154},[57,5332,5333],{},"    ) AS connect_rate_pct\n",[57,5335,5336],{"class":59,"line":175},[57,5337,5338],{},"FROM vicidial_log\n",[57,5340,5341],{"class":59,"line":190},[57,5342,5343],{},"WHERE call_date > NOW() - INTERVAL '7 days'\n",[57,5345,5346],{"class":59,"line":195},[57,5347,5348],{},"GROUP BY call_date::date\n",[57,5350,5351],{"class":59,"line":207},[57,5352,5353],{},"ORDER BY day DESC;\n",[20,5355,5357],{"id":5356},"mysql-performance-optimization","MySQL Performance Optimization",[16,5359,5360,5361,5364],{},"Vicidial is heavily database-driven. The predictive algorithm queries the ",[28,5362,5363],{},"vicidial_list"," table every second to select leads. On large campaigns (1M+ records), this query becomes the bottleneck.",[16,5366,5367],{},"Critical indexes:",[48,5369,5371],{"className":5279,"code":5370,"language":5281,"meta":53,"style":53},"-- vicidial_list: compound index for lead selection query\nALTER TABLE vicidial_list\nADD INDEX status_order_idx (status, list_id, gmt_offset_now, called_count, lead_id);\n\n-- vicidial_log: index for recent performance queries\nALTER TABLE vicidial_log\nADD INDEX campaign_date_idx (campaign_id, call_date, status);\n\n-- vicidial_agent_log: index for real-time agent stats\nALTER TABLE vicidial_agent_log\nADD INDEX agent_campaign_idx (campaign_id, event_time, user);\n",[28,5372,5373,5378,5383,5388,5392,5397,5402,5407,5411,5416,5421],{"__ignoreMap":53},[57,5374,5375],{"class":59,"line":60},[57,5376,5377],{},"-- vicidial_list: compound index for lead selection query\n",[57,5379,5380],{"class":59,"line":67},[57,5381,5382],{},"ALTER TABLE vicidial_list\n",[57,5384,5385],{"class":59,"line":81},[57,5386,5387],{},"ADD INDEX status_order_idx (status, list_id, gmt_offset_now, called_count, lead_id);\n",[57,5389,5390],{"class":59,"line":91},[57,5391,95],{"emptyLinePlaceholder":94},[57,5393,5394],{"class":59,"line":98},[57,5395,5396],{},"-- vicidial_log: index for recent performance queries\n",[57,5398,5399],{"class":59,"line":123},[57,5400,5401],{},"ALTER TABLE vicidial_log\n",[57,5403,5404],{"class":59,"line":132},[57,5405,5406],{},"ADD INDEX campaign_date_idx (campaign_id, call_date, status);\n",[57,5408,5409],{"class":59,"line":143},[57,5410,95],{"emptyLinePlaceholder":94},[57,5412,5413],{"class":59,"line":148},[57,5414,5415],{},"-- vicidial_agent_log: index for real-time agent stats\n",[57,5417,5418],{"class":59,"line":154},[57,5419,5420],{},"ALTER TABLE vicidial_agent_log\n",[57,5422,5423],{"class":59,"line":175},[57,5424,5425],{},"ADD INDEX agent_campaign_idx (campaign_id, event_time, user);\n",[16,5427,5428],{},"MySQL configuration for a dedicated Vicidial database server (32 GB RAM):",[48,5430,5432],{"className":406,"code":5431,"language":408,"meta":53,"style":53},"[mysqld]\ninnodb_buffer_pool_size = 24G        # 75% of RAM\ninnodb_buffer_pool_instances = 8\ninnodb_log_file_size = 2G\ninnodb_flush_log_at_trx_commit = 2   # Faster writes, minimal data loss risk\ninnodb_flush_method = O_DIRECT\nquery_cache_type = 0                  # Disable — Vicidial invalidates it constantly\nmax_connections = 500\nthread_cache_size = 64\ntable_open_cache = 4000\n",[28,5433,5434,5439,5444,5449,5454,5459,5464,5469,5474,5479],{"__ignoreMap":53},[57,5435,5436],{"class":59,"line":60},[57,5437,5438],{},"[mysqld]\n",[57,5440,5441],{"class":59,"line":67},[57,5442,5443],{},"innodb_buffer_pool_size = 24G        # 75% of RAM\n",[57,5445,5446],{"class":59,"line":81},[57,5447,5448],{},"innodb_buffer_pool_instances = 8\n",[57,5450,5451],{"class":59,"line":91},[57,5452,5453],{},"innodb_log_file_size = 2G\n",[57,5455,5456],{"class":59,"line":98},[57,5457,5458],{},"innodb_flush_log_at_trx_commit = 2   # Faster writes, minimal data loss risk\n",[57,5460,5461],{"class":59,"line":123},[57,5462,5463],{},"innodb_flush_method = O_DIRECT\n",[57,5465,5466],{"class":59,"line":132},[57,5467,5468],{},"query_cache_type = 0                  # Disable — Vicidial invalidates it constantly\n",[57,5470,5471],{"class":59,"line":143},[57,5472,5473],{},"max_connections = 500\n",[57,5475,5476],{"class":59,"line":148},[57,5477,5478],{},"thread_cache_size = 64\n",[57,5480,5481],{"class":59,"line":154},[57,5482,5483],{},"table_open_cache = 4000\n",[16,5485,5486,5489],{},[28,5487,5488],{},"innodb_flush_log_at_trx_commit=2"," flushes the log buffer to OS every second rather than every transaction. On Vicidial's write-heavy workload, this typically doubles write throughput with negligible durability risk (you lose at most 1 second of data on crash, and CDRs are also in Asterisk).",[20,5491,5493],{"id":5492},"real-time-agent-monitoring-via-esl","Real-Time Agent Monitoring via ESL",[16,5495,5496],{},"Vicidial's built-in monitoring dashboard polls the database, which adds 1–3 second lag. For real-time dashboards, connect directly to Asterisk's Event Socket Layer:",[48,5498,5500],{"className":1536,"code":5499,"language":1538,"meta":53,"style":53},"import socket\n\ndef monitor_active_calls():\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.connect(('127.0.0.1', 8021))\n    \n    # Authenticate\n    sock.sendall(b'auth ClueCon\\r\\n\\r\\n')\n    response = sock.recv(1024)\n    \n    # Subscribe to channel events\n    sock.sendall(b'event plain CHANNEL_CREATE CHANNEL_HANGUP CHANNEL_BRIDGE\\r\\n\\r\\n')\n    \n    while True:\n        data = sock.recv(4096).decode()\n        if 'CHANNEL_CREATE' in data:\n            process_new_call(data)\n        elif 'CHANNEL_HANGUP' in data:\n            process_hangup(data)\n        elif 'CHANNEL_BRIDGE' in data:\n            process_agent_connected(data)\n",[28,5501,5502,5507,5511,5516,5521,5526,5530,5535,5540,5545,5549,5554,5559,5563,5567,5572,5577,5582,5587,5592,5597],{"__ignoreMap":53},[57,5503,5504],{"class":59,"line":60},[57,5505,5506],{},"import socket\n",[57,5508,5509],{"class":59,"line":67},[57,5510,95],{"emptyLinePlaceholder":94},[57,5512,5513],{"class":59,"line":81},[57,5514,5515],{},"def monitor_active_calls():\n",[57,5517,5518],{"class":59,"line":91},[57,5519,5520],{},"    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",[57,5522,5523],{"class":59,"line":98},[57,5524,5525],{},"    sock.connect(('127.0.0.1', 8021))\n",[57,5527,5528],{"class":59,"line":123},[57,5529,1603],{},[57,5531,5532],{"class":59,"line":132},[57,5533,5534],{},"    # Authenticate\n",[57,5536,5537],{"class":59,"line":143},[57,5538,5539],{},"    sock.sendall(b'auth ClueCon\\r\\n\\r\\n')\n",[57,5541,5542],{"class":59,"line":148},[57,5543,5544],{},"    response = sock.recv(1024)\n",[57,5546,5547],{"class":59,"line":154},[57,5548,1603],{},[57,5550,5551],{"class":59,"line":175},[57,5552,5553],{},"    # Subscribe to channel events\n",[57,5555,5556],{"class":59,"line":190},[57,5557,5558],{},"    sock.sendall(b'event plain CHANNEL_CREATE CHANNEL_HANGUP CHANNEL_BRIDGE\\r\\n\\r\\n')\n",[57,5560,5561],{"class":59,"line":195},[57,5562,1603],{},[57,5564,5565],{"class":59,"line":207},[57,5566,1696],{},[57,5568,5569],{"class":59,"line":216},[57,5570,5571],{},"        data = sock.recv(4096).decode()\n",[57,5573,5574],{"class":59,"line":222},[57,5575,5576],{},"        if 'CHANNEL_CREATE' in data:\n",[57,5578,5579],{"class":59,"line":490},[57,5580,5581],{},"            process_new_call(data)\n",[57,5583,5584],{"class":59,"line":496},[57,5585,5586],{},"        elif 'CHANNEL_HANGUP' in data:\n",[57,5588,5589],{"class":59,"line":502},[57,5590,5591],{},"            process_hangup(data)\n",[57,5593,5594],{"class":59,"line":507},[57,5595,5596],{},"        elif 'CHANNEL_BRIDGE' in data:\n",[57,5598,5599],{"class":59,"line":513},[57,5600,5601],{},"            process_agent_connected(data)\n",[16,5603,5604],{},"ESL events arrive within 50ms of the actual channel event — fast enough to update a real-time dashboard without database polling.",[20,5606,5608],{"id":5607},"compliance-abandon-rate-enforcement","Compliance: Abandon Rate Enforcement",[16,5610,5611],{},"US FTC regulations require abandon rates below 3% per campaign per 30-day period. Vicidial's adaptive mode tracks this internally, but you should also verify from the database:",[48,5613,5615],{"className":5279,"code":5614,"language":5281,"meta":53,"style":53},"-- Monthly abandon rate by campaign (FTC calculation)\nSELECT\n    campaign_id,\n    COUNT(*) AS total_connected,\n    SUM(CASE WHEN status = 'DROP' THEN 1 ELSE 0 END) AS abandoned,\n    ROUND(\n        100.0 * SUM(CASE WHEN status = 'DROP' THEN 1 ELSE 0 END) \u002F COUNT(*),\n        2\n    ) AS abandon_rate_pct\nFROM vicidial_log\nWHERE call_date >= DATE_TRUNC('month', NOW())\n  AND status NOT IN ('AMD', 'CBHOLD', 'CFAIL')  -- Exclude non-connected\nGROUP BY campaign_id\nHAVING abandon_rate_pct > 2.5  -- Alert before hitting 3% limit\nORDER BY abandon_rate_pct DESC;\n",[28,5616,5617,5622,5626,5631,5636,5641,5645,5650,5655,5660,5664,5669,5674,5679,5684],{"__ignoreMap":53},[57,5618,5619],{"class":59,"line":60},[57,5620,5621],{},"-- Monthly abandon rate by campaign (FTC calculation)\n",[57,5623,5624],{"class":59,"line":67},[57,5625,5288],{},[57,5627,5628],{"class":59,"line":81},[57,5629,5630],{},"    campaign_id,\n",[57,5632,5633],{"class":59,"line":91},[57,5634,5635],{},"    COUNT(*) AS total_connected,\n",[57,5637,5638],{"class":59,"line":98},[57,5639,5640],{},"    SUM(CASE WHEN status = 'DROP' THEN 1 ELSE 0 END) AS abandoned,\n",[57,5642,5643],{"class":59,"line":123},[57,5644,5318],{},[57,5646,5647],{"class":59,"line":132},[57,5648,5649],{},"        100.0 * SUM(CASE WHEN status = 'DROP' THEN 1 ELSE 0 END) \u002F COUNT(*),\n",[57,5651,5652],{"class":59,"line":143},[57,5653,5654],{},"        2\n",[57,5656,5657],{"class":59,"line":148},[57,5658,5659],{},"    ) AS abandon_rate_pct\n",[57,5661,5662],{"class":59,"line":154},[57,5663,5338],{},[57,5665,5666],{"class":59,"line":175},[57,5667,5668],{},"WHERE call_date >= DATE_TRUNC('month', NOW())\n",[57,5670,5671],{"class":59,"line":190},[57,5672,5673],{},"  AND status NOT IN ('AMD', 'CBHOLD', 'CFAIL')  -- Exclude non-connected\n",[57,5675,5676],{"class":59,"line":195},[57,5677,5678],{},"GROUP BY campaign_id\n",[57,5680,5681],{"class":59,"line":207},[57,5682,5683],{},"HAVING abandon_rate_pct > 2.5  -- Alert before hitting 3% limit\n",[57,5685,5686],{"class":59,"line":216},[57,5687,5688],{},"ORDER BY abandon_rate_pct DESC;\n",[16,5690,5691],{},"Run this query from a cron job every hour and alert when any campaign approaches 2.5%. At 2.5% you have margin to reduce the dial ratio before crossing the compliance threshold.",[20,5693,5695],{"id":5694},"lead-list-hygiene","Lead List Hygiene",[16,5697,5698],{},"A clean lead list has a larger impact on agent productivity than any algorithm tuning. Before loading a list:",[785,5700,5701,5704,5707,5710],{},[788,5702,5703],{},"Scrub against the National Do Not Call Registry (required by law)",[788,5705,5706],{},"Remove duplicates by phone number within the last 30 days",[788,5708,5709],{},"Filter disconnected numbers using carrier lookup APIs (Twilio Lookup, Numverify)",[788,5711,5712],{},"Set time zone based on area code — never dial outside 8 AM–9 PM local time",[16,5714,5715],{},"A list with 20% bad numbers means 20% of your dial capacity is wasted on disconnects. Carrier lookup APIs cost $0.005–$0.01 per number. On a 100,000-number list, $500–$1,000 of cleanup saves significant agent idle time over the campaign lifetime.",[20,5717,5719],{"id":5718},"expected-results-after-tuning","Expected Results After Tuning",[661,5721,5722,5734],{},[664,5723,5724],{},[667,5725,5726,5728,5731],{},[670,5727,1750],{},[670,5729,5730],{},"Before tuning",[670,5732,5733],{},"After tuning",[677,5735,5736,5747,5758,5769,5779],{},[667,5737,5738,5741,5744],{},[682,5739,5740],{},"Agent talk time per hour",[682,5742,5743],{},"32 min",[682,5745,5746],{},"48 min",[667,5748,5749,5752,5755],{},[682,5750,5751],{},"Connect rate",[682,5753,5754],{},"18%",[682,5756,5757],{},"28%",[667,5759,5760,5763,5766],{},[682,5761,5762],{},"AMD accuracy",[682,5764,5765],{},"65%",[682,5767,5768],{},"88%",[667,5770,5771,5774,5776],{},[682,5772,5773],{},"Abandon rate",[682,5775,2803],{},[682,5777,5778],{},"2.5%",[667,5780,5781,5784,5787],{},[682,5782,5783],{},"DB query time (lead select)",[682,5785,5786],{},"800ms",[682,5788,2817],{},[16,5790,5791],{},"These numbers come from a 50-agent campaign on a list of 500,000 records after applying the indexes, AMD tuning, and adaptive dial level changes described above.",[1009,5793,3478],{},{"title":53,"searchDepth":67,"depth":67,"links":5795},[5796,5797,5798,5799,5800,5801,5802,5803],{"id":4977,"depth":67,"text":4978},{"id":5016,"depth":67,"text":5017},{"id":5178,"depth":67,"text":5179},{"id":5356,"depth":67,"text":5357},{"id":5492,"depth":67,"text":5493},{"id":5607,"depth":67,"text":5608},{"id":5694,"depth":67,"text":5695},{"id":5718,"depth":67,"text":5719},"Dialer","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1516321318423-f06f85e504b3?w=1200&q=80","2025-11-01","Tune Vicidial's predictive dialer for maximum agent productivity: dial ratio math, abandon rate compliance, MySQL query optimization, and Asterisk AGI integration patterns.",{},"\u002Fblog\u002Fvicidial-predictive-dialer-tuning",{"title":4966,"description":5807},"blog\u002Fvicidial-predictive-dialer-tuning",[5813,5814,2707,5815,5816,5817],"vicidial","predictive-dialer","call-center","dialer-tuning","agi","LruH-okX53wG4D-RlBCIffnkI_w70nwfzHsqFNLYwV0",{"id":5820,"title":5821,"author":7,"body":5822,"category":1023,"coverImage":6197,"date":6198,"description":6199,"extension":1027,"meta":6200,"navigation":94,"path":6201,"readingTime":154,"seo":6202,"stem":6203,"tags":6204,"__hash__":6205},"posts\u002Fblog\u002Fsbc-sizing-guide.md","Sizing your SBC: a practical capacity planning guide",{"type":9,"value":5823,"toc":6188},[5824,5827,5830,5833,5837,5840,5846,5852,5855,5859,5862,5924,5927,5938,5942,5945,5958,6019,6022,6036,6040,6046,6051,6059,6064,6078,6081,6087,6091,6094,6105,6108,6112,6115,6158,6162,6165,6185],[12,5825,5821],{"id":5826},"sizing-your-sbc-a-practical-capacity-planning-guide",[16,5828,5829],{},"Most SBC sizing exercises start with the vendor's datasheet and end with a box that's either oversized by 4x or hits CPU at 70% of projected load. The datasheet numbers assume G.711 calls, no transcoding, and a pristine network — none of which describe real production traffic.",[16,5831,5832],{},"Here's how to size an open-source SBC (Kamailio + rtpengine) for the traffic you'll actually carry.",[20,5834,5836],{"id":5835},"two-separate-problems","Two separate problems",[16,5838,5839],{},"SBC capacity splits cleanly into two planes:",[16,5841,5842,5845],{},[2311,5843,5844],{},"Signaling plane"," (Kamailio) — handles SIP message processing: INVITE, REGISTER, OPTIONS, BYE, and all the state machine around them. Measured in calls per second (CPS) and concurrent dialogs.",[16,5847,5848,5851],{},[2311,5849,5850],{},"Media plane"," (rtpengine) — handles RTP\u002FSRTP forwarding, transcoding, and recording. Measured in concurrent sessions and CPU cores for transcoding.",[16,5853,5854],{},"Size them independently and deploy them on separate machines. A signaling overload shouldn't kill media, and a transcoding spike shouldn't drop your SIP registrations.",[20,5856,5858],{"id":5857},"signaling-sizing-kamailio","Signaling sizing (Kamailio)",[16,5860,5861],{},"Kamailio's CPS capacity on modern hardware:",[661,5863,5864,5880],{},[664,5865,5866],{},[667,5867,5868,5871,5874,5877],{},[670,5869,5870],{},"Hardware",[670,5872,5873],{},"INVITE CPS (no DB)",[670,5875,5876],{},"INVITE CPS (with Postgres)",[670,5878,5879],{},"Concurrent dialogs",[677,5881,5882,5896,5910],{},[667,5883,5884,5887,5890,5893],{},[682,5885,5886],{},"4-core 2020-era Xeon",[682,5888,5889],{},"4,000–6,000",[682,5891,5892],{},"1,500–2,500",[682,5894,5895],{},"200,000+",[667,5897,5898,5901,5904,5907],{},[682,5899,5900],{},"8-core Xeon\u002FEPYC",[682,5902,5903],{},"10,000–15,000",[682,5905,5906],{},"4,000–7,000",[682,5908,5909],{},"500,000+",[667,5911,5912,5915,5918,5921],{},[682,5913,5914],{},"16-core EPYC",[682,5916,5917],{},"20,000–30,000",[682,5919,5920],{},"8,000–15,000",[682,5922,5923],{},"1M+",[16,5925,5926],{},"Rules of thumb:",[2315,5928,5929,5932,5935],{},[788,5930,5931],{},"If your routing decisions hit a database per INVITE, capacity drops by 50-70%. Use htable or Redis for hot lookups.",[788,5933,5934],{},"REGISTER storms (after a network partition, for example) can generate 10-50x your normal CPS. Size for the storm, not steady state.",[788,5936,5937],{},"Concurrent dialogs consume memory linearly. At ~1KB per dialog, 500k dialogs = 500MB. This is rarely the bottleneck.",[20,5939,5941],{"id":5940},"media-sizing-rtpengine","Media sizing (rtpengine)",[16,5943,5944],{},"rtpengine's media capacity is determined by:",[785,5946,5947,5952],{},[788,5948,5949,5951],{},[2311,5950,854],{}," — RTP forwarding is cheap. A single 8-core machine handles 50,000+ concurrent G.711 relay sessions without transcoding.",[788,5953,5954,5957],{},[2311,5955,5956],{},"Transcoding overhead"," — this is where you actually consume CPU. Rough per-core capacity:",[661,5959,5960,5970],{},[664,5961,5962],{},[667,5963,5964,5967],{},[670,5965,5966],{},"Codec pair",[670,5968,5969],{},"Concurrent sessions per core",[677,5971,5972,5980,5987,5995,6003,6011],{},[667,5973,5974,5977],{},[682,5975,5976],{},"G.711 relay (no transcode)",[682,5978,5979],{},"~10,000",[667,5981,5982,5985],{},[682,5983,5984],{},"G.711 μ-law ↔ a-law",[682,5986,867],{},[667,5988,5989,5992],{},[682,5990,5991],{},"G.711 ↔ G.729 (software)",[682,5993,5994],{},"~150–200",[667,5996,5997,6000],{},[682,5998,5999],{},"G.711 ↔ Opus",[682,6001,6002],{},"~80–120",[667,6004,6005,6008],{},[682,6006,6007],{},"G.711 ↔ AMR-WB",[682,6009,6010],{},"~60–100",[667,6012,6013,6016],{},[682,6014,6015],{},"G.729 ↔ Opus",[682,6017,6018],{},"~50–80",[16,6020,6021],{},"G.729 transcoding is the one that catches teams by surprise. If 30% of your calls transcode G.711 to G.729, you need 15x the CPU compared to relay-only.",[785,6023,6024,6030],{"start":81},[788,6025,6026,6029],{},[2311,6027,6028],{},"SRTP overhead"," — SRTP encryption\u002Fdecryption costs roughly 5-10% additional CPU vs. plain RTP. Negligible unless you're doing full transcoding at the same time.",[788,6031,6032,6035],{},[2311,6033,6034],{},"Recording"," — writing PCM to disk while relaying adds I\u002FO load. Budget 2-3x the RTP bandwidth in disk write throughput, and use dedicated disks or object store offload.",[20,6037,6039],{"id":6038},"working-through-an-example","Working through an example",[16,6041,6042,6045],{},[2311,6043,6044],{},"Scenario:"," 500 concurrent calls, 30% G.711↔G.729 transcoding, 70% G.711 relay, SRTP on all legs, no recording.",[16,6047,6048],{},[2311,6049,6050],{},"Signaling (Kamailio):",[2315,6052,6053,6056],{},[788,6054,6055],{},"At 500 concurrent calls with ~3-minute average duration: ~2.8 CPS. A basic 4-core Kamailio handles this comfortably — signaling is not the bottleneck.",[788,6057,6058],{},"Size for 10x: 28 CPS for spikes. Still well within a 4-core node.",[16,6060,6061],{},[2311,6062,6063],{},"Media (rtpengine):",[2315,6065,6066,6069,6072,6075],{},[788,6067,6068],{},"350 G.711 relay sessions: negligible",[788,6070,6071],{},"150 G.711↔G.729 sessions at ~175 sessions\u002Fcore = 0.86 cores",[788,6073,6074],{},"SRTP overhead: +10% = ~0.95 cores total for transcoding",[788,6076,6077],{},"Add 50% headroom: 1.5 cores for transcoding",[16,6079,6080],{},"Result: a 4-core rtpengine server handles this comfortably with room for 3x growth.",[16,6082,6083,6086],{},[2311,6084,6085],{},"Revised scenario with recording:","\nAdd recording for all 500 calls at G.711 = 64kbps per leg × 2 legs = 128kbps per call. 500 calls = 64MB\u002Fs of disk writes. This requires dedicated disk I\u002FO — don't share with OS or application disks.",[20,6088,6090],{"id":6089},"network-sizing","Network sizing",[16,6092,6093],{},"RTP bandwidth per call:",[2315,6095,6096,6099,6102],{},[788,6097,6098],{},"G.711 (20ms ptime): ~80kbps per direction",[788,6100,6101],{},"G.729 (20ms ptime): ~26kbps per direction",[788,6103,6104],{},"Opus (20ms ptime, default): ~40kbps per direction",[16,6106,6107],{},"For 500 concurrent G.711 calls (both directions through rtpengine): 500 × 2 × 80kbps = 80Mbps. A gigabit NIC handles this with headroom. Above 5,000 concurrent G.711 calls, start thinking about 10GbE and multiple NIC queues.",[20,6109,6111],{"id":6110},"hardware-selection-checklist","Hardware selection checklist",[16,6113,6114],{},"Before finalizing hardware:",[2315,6116,6119,6128,6134,6140,6146,6152],{"className":6117},[6118],"contains-task-list",[788,6120,6123,6127],{"className":6121},[6122],"task-list-item",[6124,6125],"input",{"disabled":94,"type":6126},"checkbox"," Calculate peak CPS including registration storms (not just steady-state call setup)",[788,6129,6131,6133],{"className":6130},[6122],[6124,6132],{"disabled":94,"type":6126}," Break down your codec mix — G.711 relay vs. transcoding matters by 50-100x",[788,6135,6137,6139],{"className":6136},[6122],[6124,6138],{"disabled":94,"type":6126}," Add 50% headroom minimum on both planes for traffic growth and failure scenarios",[788,6141,6143,6145],{"className":6142},[6122],[6124,6144],{"disabled":94,"type":6126}," If recording, isolate disk I\u002FO on dedicated volumes or offload to S3",[788,6147,6149,6151],{"className":6148},[6122],[6124,6150],{"disabled":94,"type":6126}," Plan HA pairs — your sizing for a single node assumes it's always up",[788,6153,6155,6157],{"className":6154},[6122],[6124,6156],{"disabled":94,"type":6126}," Test under load with SIPp before production. The numbers above are empirical, not guaranteed.",[20,6159,6161],{"id":6160},"ha-pairing","HA pairing",[16,6163,6164],{},"Deploy Kamailio and rtpengine in HA pairs with keepalived for IP failover. The specific failure modes to test:",[2315,6166,6167,6173,6179],{},[788,6168,6169,6172],{},[2311,6170,6171],{},"Kamailio node failure",": the peer takes the VIP within \u003C2s. In-progress calls can survive if you're using stateless processing (most INVITE routing is).",[788,6174,6175,6178],{},[2311,6176,6177],{},"rtpengine node failure",": in-progress calls drop at the media layer. Signal-level REINVITE is required to re-establish media. Design for this or deploy an rtpengine cluster where the partner node can take over an existing session via the ng control protocol.",[788,6180,6181,6184],{},[2311,6182,6183],{},"Database failure",": if your routing tables are in Postgres, a DB failure can take down both signaling nodes. Use htable caching with a warm-reload on DB reconnect.",[16,6186,6187],{},"Sizing is easier when you've measured. Instrument your deployment with Prometheus + Grafana from day one — knowing your actual CPS, concurrent session count, and codec mix takes all the guesswork out of capacity planning for the next upgrade.",{"title":53,"searchDepth":67,"depth":67,"links":6189},[6190,6191,6192,6193,6194,6195,6196],{"id":5835,"depth":67,"text":5836},{"id":5857,"depth":67,"text":5858},{"id":5940,"depth":67,"text":5941},{"id":6038,"depth":67,"text":6039},{"id":6089,"depth":67,"text":6090},{"id":6110,"depth":67,"text":6111},{"id":6160,"depth":67,"text":6161},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1544197150-b99a580bb7a8?w=1200&q=80","2025-10-28","How to calculate concurrent sessions, CPS limits, transcoding overhead, and hardware sizing for a Session Border Controller deployment.",{},"\u002Fblog\u002Fsbc-sizing-guide",{"title":5821,"description":6199},"blog\u002Fsbc-sizing-guide",[1034,841,647,1036],"y_UhHc56IVuqHPZBfiWnBkRd57fCUssc1W6n76VMEFM",{"id":6207,"title":6208,"author":7,"body":6209,"category":2694,"coverImage":6197,"date":6930,"description":6931,"extension":1027,"meta":6932,"navigation":94,"path":6933,"readingTime":154,"seo":6934,"stem":6935,"tags":6936,"__hash__":6942},"posts\u002Fblog\u002Ffreeswitch-high-availability.md","FreeSWITCH High Availability: Active-Active Cluster Setup",{"type":9,"value":6210,"toc":6920},[6211,6214,6217,6221,6227,6230,6234,6245,6332,6342,6346,6349,6355,6361,6367,6371,6374,6380,6383,6387,6390,6438,6448,6463,6466,6470,6473,6702,6705,6709,6712,6840,6843,6845,6914,6917],[12,6212,6208],{"id":6213},"freeswitch-high-availability-active-active-cluster-setup",[16,6215,6216],{},"FreeSWITCH does not ship with native clustering. It is designed as a single-node media server, and its internal state — active calls, channel variables, dialplan state — lives in process memory. Building high availability on top of FreeSWITCH means externalizing that state and routing around failures at the SIP proxy layer. This post covers an active-active architecture that handles node failures without dropping established calls and routes new calls away from unhealthy nodes within seconds.",[20,6218,6220],{"id":6219},"architecture-overview","Architecture Overview",[48,6222,6225],{"className":6223,"code":6224,"language":654},[652],"                    ┌─────────────────────────────┐\n   SIP Trunk ──────►│    Kamailio (load balancer)  │◄── SIP clients\n                    └────────┬────────────┬────────┘\n                             │            │\n              ┌──────────────▼──┐      ┌──▼──────────────┐\n              │  FreeSWITCH-1   │      │  FreeSWITCH-2   │\n              │  (active)       │      │  (active)        │\n              └──────────┬──────┘      └──────┬──────────┘\n                         │                    │\n                    ┌────▼────────────────────▼────┐\n                    │    PostgreSQL (shared state)  │\n                    │    + Redis (call registry)    │\n                    └──────────────────────────────┘\n",[28,6226,6224],{"__ignoreMap":53},[16,6228,6229],{},"Both FreeSWITCH nodes are active simultaneously. Kamailio distributes new calls across nodes using dispatcher. Active calls stay pinned to the node they started on — FreeSWITCH does not support live call migration between nodes. When a node fails, in-flight calls on that node drop (unavoidable without media server clustering), but new calls immediately route to the surviving node.",[20,6231,6233],{"id":6232},"freeswitch-node-configuration","FreeSWITCH Node Configuration",[16,6235,6236,6237,6240,6241,6244],{},"Each node needs a unique ",[28,6238,6239],{},"rtp-ip"," and ",[28,6242,6243],{},"sip-ip"," binding but can share the same SIP profile structure:",[48,6246,6250],{"className":6247,"code":6248,"language":6249,"meta":53,"style":53},"language-xml shiki shiki-themes github-light github-dark","\u003C!-- \u002Fetc\u002Ffreeswitch\u002Fsip_profiles\u002Fexternal.xml (Node 1) -->\n\u003Cprofile name=\"external\">\n  \u003Csettings>\n    \u003Cparam name=\"sip-ip\" value=\"10.0.1.10\"\u002F>\n    \u003Cparam name=\"rtp-ip\" value=\"10.0.1.10\"\u002F>\n    \u003Cparam name=\"ext-rtp-ip\" value=\"203.0.113.10\"\u002F>\n    \u003Cparam name=\"ext-sip-ip\" value=\"203.0.113.10\"\u002F>\n    \u003Cparam name=\"sip-port\" value=\"5080\"\u002F>\n    \u003Cparam name=\"rtp-start-port\" value=\"16384\"\u002F>\n    \u003Cparam name=\"rtp-end-port\" value=\"32768\"\u002F>\n    \u003Cparam name=\"apply-nat-acl\" value=\"rfc1918\"\u002F>\n    \u003Cparam name=\"manage-presence\" value=\"false\"\u002F>\n    \u003C!-- Unique node identifier for call routing -->\n    \u003Cparam name=\"user-agent-string\" value=\"FreeSWITCH\u002Fnode-1\"\u002F>\n  \u003C\u002Fsettings>\n\u003C\u002Fprofile>\n","xml",[28,6251,6252,6257,6262,6267,6272,6277,6282,6287,6292,6297,6302,6307,6312,6317,6322,6327],{"__ignoreMap":53},[57,6253,6254],{"class":59,"line":60},[57,6255,6256],{},"\u003C!-- \u002Fetc\u002Ffreeswitch\u002Fsip_profiles\u002Fexternal.xml (Node 1) -->\n",[57,6258,6259],{"class":59,"line":67},[57,6260,6261],{},"\u003Cprofile name=\"external\">\n",[57,6263,6264],{"class":59,"line":81},[57,6265,6266],{},"  \u003Csettings>\n",[57,6268,6269],{"class":59,"line":91},[57,6270,6271],{},"    \u003Cparam name=\"sip-ip\" value=\"10.0.1.10\"\u002F>\n",[57,6273,6274],{"class":59,"line":98},[57,6275,6276],{},"    \u003Cparam name=\"rtp-ip\" value=\"10.0.1.10\"\u002F>\n",[57,6278,6279],{"class":59,"line":123},[57,6280,6281],{},"    \u003Cparam name=\"ext-rtp-ip\" value=\"203.0.113.10\"\u002F>\n",[57,6283,6284],{"class":59,"line":132},[57,6285,6286],{},"    \u003Cparam name=\"ext-sip-ip\" value=\"203.0.113.10\"\u002F>\n",[57,6288,6289],{"class":59,"line":143},[57,6290,6291],{},"    \u003Cparam name=\"sip-port\" value=\"5080\"\u002F>\n",[57,6293,6294],{"class":59,"line":148},[57,6295,6296],{},"    \u003Cparam name=\"rtp-start-port\" value=\"16384\"\u002F>\n",[57,6298,6299],{"class":59,"line":154},[57,6300,6301],{},"    \u003Cparam name=\"rtp-end-port\" value=\"32768\"\u002F>\n",[57,6303,6304],{"class":59,"line":175},[57,6305,6306],{},"    \u003Cparam name=\"apply-nat-acl\" value=\"rfc1918\"\u002F>\n",[57,6308,6309],{"class":59,"line":190},[57,6310,6311],{},"    \u003Cparam name=\"manage-presence\" value=\"false\"\u002F>\n",[57,6313,6314],{"class":59,"line":195},[57,6315,6316],{},"    \u003C!-- Unique node identifier for call routing -->\n",[57,6318,6319],{"class":59,"line":207},[57,6320,6321],{},"    \u003Cparam name=\"user-agent-string\" value=\"FreeSWITCH\u002Fnode-1\"\u002F>\n",[57,6323,6324],{"class":59,"line":216},[57,6325,6326],{},"  \u003C\u002Fsettings>\n",[57,6328,6329],{"class":59,"line":222},[57,6330,6331],{},"\u003C\u002Fprofile>\n",[16,6333,6334,6335,6240,6338,6341],{},"Node 2 mirrors this with ",[28,6336,6337],{},"10.0.1.11",[28,6339,6340],{},"203.0.113.11",". Keep RTP port ranges non-overlapping between nodes if they share any network segment.",[20,6343,6345],{"id":6344},"kamailio-dispatcher-configuration","Kamailio Dispatcher Configuration",[16,6347,6348],{},"Kamailio acts as the SIP load balancer. Configure dispatcher to probe both FreeSWITCH nodes:",[48,6350,6353],{"className":6351,"code":6352,"language":654},[652],"# \u002Fetc\u002Fkamailio\u002Fdispatcher.list\n# setid  destination                    flags  priority\n1        sip:10.0.1.10:5080             0      10\n1        sip:10.0.1.11:5080             0      10\n",[28,6354,6352],{"__ignoreMap":53},[48,6356,6359],{"className":6357,"code":6358,"language":654},[652],"# kamailio.cfg — relevant dispatcher section\nloadmodule \"dispatcher.so\"\n\nmodparam(\"dispatcher\", \"list_file\", \"\u002Fetc\u002Fkamailio\u002Fdispatcher.list\")\nmodparam(\"dispatcher\", \"probing_mode\", 1)\nmodparam(\"dispatcher\", \"ds_ping_method\", \"OPTIONS\")\nmodparam(\"dispatcher\", \"ds_ping_from\", \"sip:monitor@kamailio.example.com\")\nmodparam(\"dispatcher\", \"ds_ping_interval\", 10)\nmodparam(\"dispatcher\", \"ds_probing_threshold\", 3)\nmodparam(\"dispatcher\", \"ds_inactive_threshold\", 3)\nmodparam(\"dispatcher\", \"ds_timeout_after_inactive\", 900)\n\nrequest_route {\n    if (is_method(\"INVITE\") && !has_totag()) {\n        # New call — load balance across active FS nodes\n        if (!ds_select_dst(1, 4)) {\n            send_reply(\"503\", \"Service Unavailable\");\n            exit;\n        }\n        t_on_failure(\"DISPATCH_FAILURE\");\n    } else if (has_totag()) {\n        # In-dialog request — route to same node\n        if (!ds_is_from_list()) {\n            # From client — forward to the FS node that owns this dialog\n            route(ROUTE_TO_FS_NODE);\n        }\n    }\n    t_relay();\n}\n\nfailure_route[DISPATCH_FAILURE] {\n    if (t_is_canceled()) exit;\n    if (t_check_status(\"503\") || t_branch_timeout()) {\n        if (ds_next_dst()) {\n            t_on_failure(\"DISPATCH_FAILURE\");\n            t_relay();\n            exit;\n        }\n    }\n    send_reply(\"503\", \"All media servers unavailable\");\n}\n",[28,6360,6358],{"__ignoreMap":53},[16,6362,629,6363,6366],{},[28,6364,6365],{},"ds_probing_threshold=3"," means a node must fail 3 consecutive OPTIONS probes (30 seconds) before being marked inactive. Adjust down to 1 for faster failover detection at the cost of brief false-positives during network blips.",[20,6368,6370],{"id":6369},"call-pinning-with-redis","Call Pinning with Redis",[16,6372,6373],{},"In-dialog requests (re-INVITE, BYE, REFER) must reach the same FreeSWITCH node that answered the original INVITE. Store the call-to-node mapping in Redis:",[48,6375,6378],{"className":6376,"code":6377,"language":654},[652],"# kamailio.cfg — store FS node on call answer\nonreply_route[STORE_NODE] {\n    if (t_check_status(\"200\")) {\n        $var(dialog_id) = $ci;\n        $var(fs_node) = $du;\n        redis_cmd(\"SET\", \"call:$var(dialog_id)\", \"$var(fs_node)\", \"EX\", \"7200\");\n    }\n}\n\nroute[ROUTE_TO_FS_NODE] {\n    $var(dialog_id) = $ci;\n    redis_cmd(\"GET\", \"call:$var(dialog_id)\");\n    if ($redis(reply) != $null) {\n        $du = $redis(reply);\n        t_relay();\n        exit;\n    }\n    # Dialog not in Redis — node may have failed\n    send_reply(\"481\", \"Call Leg\u002FTransaction Does Not Exist\");\n}\n",[28,6379,6377],{"__ignoreMap":53},[16,6381,6382],{},"Set the Redis key TTL to your maximum call duration (7200 seconds = 2 hours). After TTL, Kamailio cleans up automatically without a separate cleanup job.",[20,6384,6386],{"id":6385},"postgresql-shared-state","PostgreSQL Shared State",[16,6388,6389],{},"FreeSWITCH uses a local SQLite database by default. Switch to PostgreSQL for shared state between nodes:",[48,6391,6393],{"className":6247,"code":6392,"language":6249,"meta":53,"style":53},"\u003C!-- \u002Fetc\u002Ffreeswitch\u002Fautoload_configs\u002Fswitch.conf.xml -->\n\u003Cconfiguration name=\"switch.conf\">\n  \u003Csettings>\n    \u003Cparam name=\"core-db-name\" value=\"\"\u002F>\n    \u003Cparam name=\"core-db-dsn\" value=\"pgsql:\u002F\u002Fuser=freeswitch;password=secret;host=db.example.com;dbname=freeswitch;\"\u002F>\n    \u003Cparam name=\"auto-create-schemas\" value=\"true\"\u002F>\n    \u003Cparam name=\"auto-clear-sql\" value=\"true\"\u002F>\n  \u003C\u002Fsettings>\n\u003C\u002Fconfiguration>\n",[28,6394,6395,6400,6405,6409,6414,6419,6424,6429,6433],{"__ignoreMap":53},[57,6396,6397],{"class":59,"line":60},[57,6398,6399],{},"\u003C!-- \u002Fetc\u002Ffreeswitch\u002Fautoload_configs\u002Fswitch.conf.xml -->\n",[57,6401,6402],{"class":59,"line":67},[57,6403,6404],{},"\u003Cconfiguration name=\"switch.conf\">\n",[57,6406,6407],{"class":59,"line":81},[57,6408,6266],{},[57,6410,6411],{"class":59,"line":91},[57,6412,6413],{},"    \u003Cparam name=\"core-db-name\" value=\"\"\u002F>\n",[57,6415,6416],{"class":59,"line":98},[57,6417,6418],{},"    \u003Cparam name=\"core-db-dsn\" value=\"pgsql:\u002F\u002Fuser=freeswitch;password=secret;host=db.example.com;dbname=freeswitch;\"\u002F>\n",[57,6420,6421],{"class":59,"line":123},[57,6422,6423],{},"    \u003Cparam name=\"auto-create-schemas\" value=\"true\"\u002F>\n",[57,6425,6426],{"class":59,"line":132},[57,6427,6428],{},"    \u003Cparam name=\"auto-clear-sql\" value=\"true\"\u002F>\n",[57,6430,6431],{"class":59,"line":143},[57,6432,6326],{},[57,6434,6435],{"class":59,"line":148},[57,6436,6437],{},"\u003C\u002Fconfiguration>\n",[16,6439,6440,6441,6240,6444,6447],{},"Also configure ",[28,6442,6443],{},"mod_voicemail",[28,6445,6446],{},"mod_sofia"," to use the shared database:",[48,6449,6451],{"className":6247,"code":6450,"language":6249,"meta":53,"style":53},"\u003C!-- sofia.conf.xml -->\n\u003Cparam name=\"db-dsn\" value=\"pgsql:\u002F\u002Fuser=freeswitch;password=secret;host=db.example.com;dbname=freeswitch;\"\u002F>\n",[28,6452,6453,6458],{"__ignoreMap":53},[57,6454,6455],{"class":59,"line":60},[57,6456,6457],{},"\u003C!-- sofia.conf.xml -->\n",[57,6459,6460],{"class":59,"line":67},[57,6461,6462],{},"\u003Cparam name=\"db-dsn\" value=\"pgsql:\u002F\u002Fuser=freeswitch;password=secret;host=db.example.com;dbname=freeswitch;\"\u002F>\n",[16,6464,6465],{},"With shared PostgreSQL, SIP registrations written by Node 1 are visible to Node 2. A registered user can reach their endpoint even if the node they registered against goes down.",[20,6467,6469],{"id":6468},"health-checks-and-monitoring","Health Checks and Monitoring",[16,6471,6472],{},"FreeSWITCH exposes an ESL (Event Socket Layer) interface for health checks. A lightweight health check script:",[48,6474,6476],{"className":50,"code":6475,"language":52,"meta":53,"style":53},"#!\u002Fbin\u002Fbash\n# \u002Fusr\u002Flocal\u002Fbin\u002Ffs-healthcheck.sh\n# Returns 0 if healthy, 1 if not — used by Kamailio OPTIONS response\n\nFS_STATUS=$(fs_cli -x \"status\" 2>\u002Fdev\u002Fnull | grep -c \"READY\")\nACTIVE_CALLS=$(fs_cli -x \"show calls count\" 2>\u002Fdev\u002Fnull | grep -oP '\\d+(?= total)')\n\nif [ \"$FS_STATUS\" -eq 0 ]; then\n    echo \"FreeSWITCH not ready\"\n    exit 1\nfi\n\n# Alert if calls exceed node capacity\nif [ \"${ACTIVE_CALLS:-0}\" -gt 500 ]; then\n    echo \"Node at capacity: ${ACTIVE_CALLS} calls\"\n    exit 1\nfi\n\necho \"OK: ${ACTIVE_CALLS} active calls\"\nexit 0\n",[28,6477,6478,6483,6488,6493,6497,6534,6566,6570,6598,6606,6613,6618,6622,6627,6656,6668,6674,6678,6682,6694],{"__ignoreMap":53},[57,6479,6480],{"class":59,"line":60},[57,6481,6482],{"class":63},"#!\u002Fbin\u002Fbash\n",[57,6484,6485],{"class":59,"line":67},[57,6486,6487],{"class":63},"# \u002Fusr\u002Flocal\u002Fbin\u002Ffs-healthcheck.sh\n",[57,6489,6490],{"class":59,"line":81},[57,6491,6492],{"class":63},"# Returns 0 if healthy, 1 if not — used by Kamailio OPTIONS response\n",[57,6494,6495],{"class":59,"line":91},[57,6496,95],{"emptyLinePlaceholder":94},[57,6498,6499,6502,6505,6507,6510,6513,6516,6519,6522,6524,6526,6529,6532],{"class":59,"line":98},[57,6500,6501],{"class":254},"FS_STATUS",[57,6503,6504],{"class":84},"=",[57,6506,255],{"class":254},[57,6508,6509],{"class":101},"fs_cli",[57,6511,6512],{"class":70}," -x",[57,6514,6515],{"class":74}," \"status\"",[57,6517,6518],{"class":84}," 2>",[57,6520,6521],{"class":74},"\u002Fdev\u002Fnull",[57,6523,362],{"class":84},[57,6525,365],{"class":101},[57,6527,6528],{"class":70}," -c",[57,6530,6531],{"class":74}," \"READY\"",[57,6533,264],{"class":254},[57,6535,6536,6539,6541,6543,6545,6547,6550,6552,6554,6556,6558,6561,6564],{"class":59,"line":123},[57,6537,6538],{"class":254},"ACTIVE_CALLS",[57,6540,6504],{"class":84},[57,6542,255],{"class":254},[57,6544,6509],{"class":101},[57,6546,6512],{"class":70},[57,6548,6549],{"class":74}," \"show calls count\"",[57,6551,6518],{"class":84},[57,6553,6521],{"class":74},[57,6555,362],{"class":84},[57,6557,365],{"class":101},[57,6559,6560],{"class":70}," -oP",[57,6562,6563],{"class":74}," '\\d+(?= total)'",[57,6565,264],{"class":254},[57,6567,6568],{"class":59,"line":132},[57,6569,95],{"emptyLinePlaceholder":94},[57,6571,6572,6575,6578,6581,6584,6586,6589,6592,6595],{"class":59,"line":143},[57,6573,6574],{"class":84},"if",[57,6576,6577],{"class":254}," [ ",[57,6579,6580],{"class":74},"\"",[57,6582,6583],{"class":254},"$FS_STATUS",[57,6585,6580],{"class":74},[57,6587,6588],{"class":84}," -eq",[57,6590,6591],{"class":70}," 0",[57,6593,6594],{"class":254}," ]; ",[57,6596,6597],{"class":84},"then\n",[57,6599,6600,6603],{"class":59,"line":148},[57,6601,6602],{"class":70},"    echo",[57,6604,6605],{"class":74}," \"FreeSWITCH not ready\"\n",[57,6607,6608,6611],{"class":59,"line":154},[57,6609,6610],{"class":70},"    exit",[57,6612,4871],{"class":70},[57,6614,6615],{"class":59,"line":175},[57,6616,6617],{"class":84},"fi\n",[57,6619,6620],{"class":59,"line":190},[57,6621,95],{"emptyLinePlaceholder":94},[57,6623,6624],{"class":59,"line":195},[57,6625,6626],{"class":63},"# Alert if calls exceed node capacity\n",[57,6628,6629,6631,6633,6636,6638,6641,6643,6646,6649,6652,6654],{"class":59,"line":207},[57,6630,6574],{"class":84},[57,6632,6577],{"class":254},[57,6634,6635],{"class":74},"\"${",[57,6637,6538],{"class":254},[57,6639,6640],{"class":84},":-",[57,6642,4626],{"class":254},[57,6644,6645],{"class":74},"}\"",[57,6647,6648],{"class":84}," -gt",[57,6650,6651],{"class":70}," 500",[57,6653,6594],{"class":254},[57,6655,6597],{"class":84},[57,6657,6658,6660,6663,6665],{"class":59,"line":216},[57,6659,6602],{"class":70},[57,6661,6662],{"class":74}," \"Node at capacity: ${",[57,6664,6538],{"class":254},[57,6666,6667],{"class":74},"} calls\"\n",[57,6669,6670,6672],{"class":59,"line":222},[57,6671,6610],{"class":70},[57,6673,4871],{"class":70},[57,6675,6676],{"class":59,"line":490},[57,6677,6617],{"class":84},[57,6679,6680],{"class":59,"line":496},[57,6681,95],{"emptyLinePlaceholder":94},[57,6683,6684,6686,6689,6691],{"class":59,"line":502},[57,6685,71],{"class":70},[57,6687,6688],{"class":74}," \"OK: ${",[57,6690,6538],{"class":254},[57,6692,6693],{"class":74},"} active calls\"\n",[57,6695,6696,6699],{"class":59,"line":507},[57,6697,6698],{"class":70},"exit",[57,6700,6701],{"class":70}," 0\n",[16,6703,6704],{},"Run this every 10 seconds from a systemd timer and expose the result via a lightweight HTTP endpoint that Kamailio's OPTIONS probe can hit. Kamailio marks the node inactive when OPTIONS responses stop, which happens automatically when the health check kills the FreeSWITCH OPTIONS response.",[20,6706,6708],{"id":6707},"graceful-drain-before-maintenance","Graceful Drain Before Maintenance",[16,6710,6711],{},"Before taking a node down for maintenance, drain it rather than killing it:",[48,6713,6715],{"className":50,"code":6714,"language":52,"meta":53,"style":53},"# Tell Kamailio to stop sending new calls to this node\nkamcmd dispatcher.set_state ip 10.0.1.10 5080 inactive\n\n# Wait for active calls to finish (check every 30 seconds)\nwhile [ $(fs_cli -x \"show calls count\" | grep -oP '\\d+(?= total)') -gt 0 ]; do\n    echo \"Waiting for calls to finish...\"\n    sleep 30\ndone\n\n# Safe to restart now\nsystemctl restart freeswitch\nkamcmd dispatcher.set_state ip 10.0.1.10 5080 active\n",[28,6716,6717,6722,6741,6745,6750,6785,6792,6800,6805,6809,6814,6825],{"__ignoreMap":53},[57,6718,6719],{"class":59,"line":60},[57,6720,6721],{"class":63},"# Tell Kamailio to stop sending new calls to this node\n",[57,6723,6724,6726,6729,6732,6735,6738],{"class":59,"line":67},[57,6725,4658],{"class":101},[57,6727,6728],{"class":74}," dispatcher.set_state",[57,6730,6731],{"class":74}," ip",[57,6733,6734],{"class":70}," 10.0.1.10",[57,6736,6737],{"class":70}," 5080",[57,6739,6740],{"class":74}," inactive\n",[57,6742,6743],{"class":59,"line":81},[57,6744,95],{"emptyLinePlaceholder":94},[57,6746,6747],{"class":59,"line":91},[57,6748,6749],{"class":63},"# Wait for active calls to finish (check every 30 seconds)\n",[57,6751,6752,6755,6758,6760,6762,6764,6766,6768,6770,6772,6775,6778,6780,6782],{"class":59,"line":98},[57,6753,6754],{"class":84},"while",[57,6756,6757],{"class":254}," [ $(",[57,6759,6509],{"class":101},[57,6761,6512],{"class":70},[57,6763,6549],{"class":74},[57,6765,362],{"class":84},[57,6767,365],{"class":101},[57,6769,6560],{"class":70},[57,6771,6563],{"class":74},[57,6773,6774],{"class":254},") ",[57,6776,6777],{"class":84},"-gt",[57,6779,6591],{"class":70},[57,6781,6594],{"class":254},[57,6783,6784],{"class":84},"do\n",[57,6786,6787,6789],{"class":59,"line":123},[57,6788,6602],{"class":70},[57,6790,6791],{"class":74}," \"Waiting for calls to finish...\"\n",[57,6793,6794,6797],{"class":59,"line":132},[57,6795,6796],{"class":101},"    sleep",[57,6798,6799],{"class":70}," 30\n",[57,6801,6802],{"class":59,"line":143},[57,6803,6804],{"class":84},"done\n",[57,6806,6807],{"class":59,"line":148},[57,6808,95],{"emptyLinePlaceholder":94},[57,6810,6811],{"class":59,"line":154},[57,6812,6813],{"class":63},"# Safe to restart now\n",[57,6815,6816,6819,6822],{"class":59,"line":175},[57,6817,6818],{"class":101},"systemctl",[57,6820,6821],{"class":74}," restart",[57,6823,6824],{"class":74}," freeswitch\n",[57,6826,6827,6829,6831,6833,6835,6837],{"class":59,"line":190},[57,6828,4658],{"class":101},[57,6830,6728],{"class":74},[57,6832,6731],{"class":74},[57,6834,6734],{"class":70},[57,6836,6737],{"class":70},[57,6838,6839],{"class":74}," active\n",[16,6841,6842],{},"This gives existing calls up to their natural duration to finish before the node goes offline. New calls route to the peer node during the drain window.",[20,6844,842],{"id":841},[661,6846,6847,6859],{},[664,6848,6849],{},[667,6850,6851,6853,6856],{},[670,6852,1750],{},[670,6854,6855],{},"Per FreeSWITCH node",[670,6857,6858],{},"2-node cluster",[677,6860,6861,6872,6883,6892,6903],{},[667,6862,6863,6866,6869],{},[682,6864,6865],{},"Concurrent calls (audio only)",[682,6867,6868],{},"500",[682,6870,6871],{},"1,000",[667,6873,6874,6877,6880],{},[682,6875,6876],{},"Concurrent calls (HD video transcode)",[682,6878,6879],{},"50",[682,6881,6882],{},"100",[667,6884,6885,6888,6890],{},[682,6886,6887],{},"INVITE\u002Fsec burst",[682,6889,6879],{},[682,6891,6882],{},[667,6893,6894,6897,6900],{},[682,6895,6896],{},"Memory per call",[682,6898,6899],{},"~2 MB",[682,6901,6902],{},"—",[667,6904,6905,6908,6911],{},[682,6906,6907],{},"Recommended RAM",[682,6909,6910],{},"16 GB",[682,6912,6913],{},"16 GB × 2",[16,6915,6916],{},"Scale horizontally by adding nodes to the Kamailio dispatcher list. The Redis and PostgreSQL backends scale independently — use a managed cloud database service (RDS, Cloud SQL) to decouple their capacity from the media server tier.",[1009,6918,6919],{},"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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":53,"searchDepth":67,"depth":67,"links":6921},[6922,6923,6924,6925,6926,6927,6928,6929],{"id":6219,"depth":67,"text":6220},{"id":6232,"depth":67,"text":6233},{"id":6344,"depth":67,"text":6345},{"id":6369,"depth":67,"text":6370},{"id":6385,"depth":67,"text":6386},{"id":6468,"depth":67,"text":6469},{"id":6707,"depth":67,"text":6708},{"id":841,"depth":67,"text":842},"2025-10-01","Configure FreeSWITCH active-active high availability using NAT traversal, shared SIP profile, PostgreSQL backend, and Kamailio load balancer for zero-downtime VoIP platforms.",{},"\u002Fblog\u002Ffreeswitch-high-availability",{"title":6208,"description":6931},"blog\u002Ffreeswitch-high-availability",[6937,6938,6939,1036,6940,6941],"freeswitch","high-availability","clustering","postgresql","voip-architecture","nMXBymkhBYz5IuNOVcko-pEMbBn8u9FELaRndldNuxw",{"id":6944,"title":6945,"author":7,"body":6946,"category":3495,"coverImage":7300,"date":7301,"description":7302,"extension":1027,"meta":7303,"navigation":94,"path":7304,"readingTime":132,"seo":7305,"stem":7306,"tags":7307,"__hash__":7310},"posts\u002Fblog\u002Fwebrtc-vs-sip-trunking.md","When to choose WebRTC vs SIP trunking for your voice app",{"type":9,"value":6947,"toc":7288},[6948,6951,6954,6960,6964,6969,6983,6990,6996,7007,7012,7016,7019,7024,7032,7037,7048,7053,7061,7066,7074,7078,7082,7088,7091,7095,7101,7104,7108,7114,7117,7121,7127,7130,7134,7258,7262,7265,7268,7271,7274,7277],[12,6949,6945],{"id":6950},"when-to-choose-webrtc-vs-sip-trunking-for-your-voice-app",[16,6952,6953],{},"Teams building voice into their applications usually frame this as \"WebRTC vs SIP\" — but that's not quite the right question. WebRTC is a browser\u002Fapp protocol for real-time media. SIP trunking is a carrier-connectivity model. You can have both, and for most serious voice applications, you will.",[16,6955,6956,6957],{},"The real question is: ",[2311,6958,6959],{},"which model carries which leg of the call, and what does that mean for your architecture?",[20,6961,6963],{"id":6962},"what-each-technology-actually-does","What each technology actually does",[16,6965,6966,6968],{},[2311,6967,3495],{}," is an open standard for real-time audio, video, and data in browsers and native apps. It handles:",[2315,6970,6971,6974,6977,6980],{},[788,6972,6973],{},"Peer-to-peer and SFU-based media between endpoints",[788,6975,6976],{},"NAT traversal via ICE\u002FSTUN\u002FTURN",[788,6978,6979],{},"Media security via DTLS-SRTP (mandatory)",[788,6981,6982],{},"Codec negotiation (Opus, VP8\u002FVP9\u002FAV1, G.711)",[16,6984,6985,6986,6989],{},"It does ",[2311,6987,6988],{},"not"," handle PSTN connectivity. A WebRTC call between two browser tabs works. A WebRTC call from a browser tab to a landline requires a WebRTC-to-SIP gateway.",[16,6991,6992,6995],{},[2311,6993,6994],{},"SIP trunking"," is a way to connect your phone system to the public telephone network (PSTN). It handles:",[2315,6997,6998,7001,7004],{},[788,6999,7000],{},"PSTN termination and origination",[788,7002,7003],{},"Number (DID) management",[788,7005,7006],{},"Compliance requirements (STIR\u002FSHAKEN, CNAM, E911)",[16,7008,6985,7009,7011],{},[2311,7010,6988],{}," define the client protocol. Your clients can be SIP hardphones, WebRTC browsers, or proprietary apps — SIP trunking is about the carrier leg, not the endpoint leg.",[20,7013,7015],{"id":7014},"the-decision-framework","The decision framework",[16,7017,7018],{},"Answer these four questions:",[16,7020,7021],{},[2311,7022,7023],{},"1. Do your users need to call or receive calls from regular phone numbers?",[2315,7025,7026,7029],{},[788,7027,7028],{},"Yes → you need SIP trunking, regardless of what your app endpoints use",[788,7030,7031],{},"No (app-to-app only) → SIP trunking is optional",[16,7033,7034],{},[2311,7035,7036],{},"2. Where are your users?",[2315,7038,7039,7042,7045],{},[788,7040,7041],{},"In a browser or mobile app → WebRTC is the right endpoint protocol",[788,7043,7044],{},"On a desk phone or SIP softphone → native SIP works; WebRTC adds complexity for no gain",[788,7046,7047],{},"Mixed → you'll need both, bridged at a media gateway",[16,7049,7050],{},[2311,7051,7052],{},"3. What are your latency and quality requirements?",[2315,7054,7055,7058],{},[788,7056,7057],{},"Sub-200ms round-trip, high quality → WebRTC with Opus, direct P2P or SFU",[788,7059,7060],{},"\"Phone quality\" acceptable → G.711 SIP is fine; Opus via WebRTC is better but overkill",[16,7062,7063],{},[2311,7064,7065],{},"4. Do you have regulatory requirements (E911, recording, STIR\u002FSHAKEN)?",[2315,7067,7068,7071],{},[788,7069,7070],{},"Yes → SIP trunking with a certified SBC; WebRTC-only architectures don't expose these hooks easily",[788,7072,7073],{},"No → either works",[20,7075,7077],{"id":7076},"common-architectures","Common architectures",[1385,7079,7081],{"id":7080},"app-to-app-only-no-pstn","App-to-app only (no PSTN)",[48,7083,7086],{"className":7084,"code":7085,"language":654},[652],"Browser\u002FApp → WebRTC → SFU (LiveKit) → Browser\u002FApp\n",[28,7087,7085],{"__ignoreMap":53},[16,7089,7090],{},"Use when: team chat, video conferencing, in-app voice, gaming comms. PSTN never enters the picture. WebRTC handles everything. Cost: SFU infrastructure + TURN servers.",[1385,7092,7094],{"id":7093},"app-to-pstn-hybrid","App-to-PSTN (hybrid)",[48,7096,7099],{"className":7097,"code":7098,"language":654},[652],"Browser\u002FApp → WebRTC → Gateway (FreeSWITCH\u002FKamailio) → SIP Trunk → PSTN\n",[28,7100,7098],{"__ignoreMap":53},[16,7102,7103],{},"Use when: click-to-call from a web app, browser-based contact center agents, customer support tools. The gateway handles the WebRTC-to-SIP protocol translation and the SRTP-to-RTP encryption translation. This is the most common architecture for customer-facing voice apps.",[1385,7105,7107],{"id":7106},"pstn-to-pstn-with-webrtc-monitoring","PSTN-to-PSTN with WebRTC monitoring",[48,7109,7112],{"className":7110,"code":7111,"language":654},[652],"Phone → SIP Trunk → SBC → FreeSWITCH → SIP Trunk → Phone\n                              ↓\n                     WebRTC monitoring tap\n",[28,7113,7111],{"__ignoreMap":53},[16,7115,7116],{},"Use when: contact centers that want browser-based supervisor tools listening to SIP calls. The core call path is SIP; WebRTC is a tap layer, not the primary transport.",[1385,7118,7120],{"id":7119},"pure-sip-no-webrtc","Pure SIP (no WebRTC)",[48,7122,7125],{"className":7123,"code":7124,"language":654},[652],"SIP Phone → SBC → PBX → SIP Trunk → PSTN\n",[28,7126,7124],{"__ignoreMap":53},[16,7128,7129],{},"Use when: enterprise PBX replacement, desk phone deployments, traditional carrier services. If your users are on desk phones and your use case is traditional telephony, adding WebRTC introduces complexity with no user-facing benefit.",[20,7131,7133],{"id":7132},"comparison-table","Comparison table",[661,7135,7136,7148],{},[664,7137,7138],{},[667,7139,7140,7143,7145],{},[670,7141,7142],{},"Dimension",[670,7144,3495],{},[670,7146,7147],{},"SIP Trunking",[677,7149,7150,7161,7172,7183,7193,7204,7215,7226,7237,7248],{},[667,7151,7152,7155,7158],{},[682,7153,7154],{},"Browser\u002Fapp native",[682,7156,7157],{},"✅ Yes",[682,7159,7160],{},"❌ Requires SIP stack",[667,7162,7163,7166,7169],{},[682,7164,7165],{},"PSTN connectivity",[682,7167,7168],{},"❌ Requires gateway",[682,7170,7171],{},"✅ Native",[667,7173,7174,7177,7180],{},[682,7175,7176],{},"E911 compliance",[682,7178,7179],{},"❌ Complex",[682,7181,7182],{},"✅ Standard",[667,7184,7185,7188,7191],{},[682,7186,7187],{},"STIR\u002FSHAKEN",[682,7189,7190],{},"❌ N\u002FA",[682,7192,7182],{},[667,7194,7195,7198,7201],{},[682,7196,7197],{},"Call recording (legal)",[682,7199,7200],{},"⚠️ App-layer",[682,7202,7203],{},"✅ Network-layer",[667,7205,7206,7209,7212],{},[682,7207,7208],{},"Codec flexibility",[682,7210,7211],{},"✅ Opus, VP8, etc.",[682,7213,7214],{},"⚠️ G.711\u002FG.729 typical",[667,7216,7217,7220,7223],{},[682,7218,7219],{},"NAT traversal",[682,7221,7222],{},"✅ ICE\u002FSTUN\u002FTURN",[682,7224,7225],{},"⚠️ Requires SBC",[667,7227,7228,7231,7234],{},[682,7229,7230],{},"Encryption",[682,7232,7233],{},"✅ DTLS-SRTP mandatory",[682,7235,7236],{},"⚠️ SRTP optional",[667,7238,7239,7242,7245],{},[682,7240,7241],{},"Desk phone support",[682,7243,7244],{},"❌",[682,7246,7247],{},"✅",[667,7249,7250,7253,7255],{},[682,7251,7252],{},"Setup complexity",[682,7254,3458],{},[682,7256,7257],{},"Low–Medium",[20,7259,7261],{"id":7260},"what-most-teams-actually-ship","What most teams actually ship",[16,7263,7264],{},"A WebRTC front-end (browser or mobile app) bridged to a SIP trunk via a media gateway. The gateway is FreeSWITCH or Asterisk for simpler cases, Kamailio + rtpengine for high-volume carrier-grade work.",[16,7266,7267],{},"The WebRTC side gives you: a great browser experience, Opus codec quality, mandatory encryption, and no SIP client to install.",[16,7269,7270],{},"The SIP trunk side gives you: PSTN access, real phone numbers, E911, STIR\u002FSHAKEN attestation, and carrier-grade reliability for the PSTN leg.",[16,7272,7273],{},"The gateway in between handles: SRTP↔RTP transcoding, SDP negotiation differences, codec normalization, and the signaling protocol translation (SIP-over-WebSocket on the WebRTC side, UDP\u002FTCP SIP on the trunk side).",[16,7275,7276],{},"If you're building a new voice application today:",[2315,7278,7279,7282,7285],{},[788,7280,7281],{},"If you have browser or mobile app endpoints → use WebRTC on the client side",[788,7283,7284],{},"If you need PSTN access → add SIP trunking on the carrier side",[788,7286,7287],{},"If you need both → deploy a media gateway. This is the right call 90% of the time, not a compromise.",{"title":53,"searchDepth":67,"depth":67,"links":7289},[7290,7291,7292,7298,7299],{"id":6962,"depth":67,"text":6963},{"id":7014,"depth":67,"text":7015},{"id":7076,"depth":67,"text":7077,"children":7293},[7294,7295,7296,7297],{"id":7080,"depth":81,"text":7081},{"id":7093,"depth":81,"text":7094},{"id":7106,"depth":81,"text":7107},{"id":7119,"depth":81,"text":7120},{"id":7132,"depth":67,"text":7133},{"id":7260,"depth":67,"text":7261},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1451187580459-43490279c0fa?w=1200&q=80","2025-09-10","A decision framework for product teams choosing between WebRTC and SIP trunking — including architecture tradeoffs, use cases, and a comparison table.",{},"\u002Fblog\u002Fwebrtc-vs-sip-trunking",{"title":6945,"description":7302},"blog\u002Fwebrtc-vs-sip-trunking",[3504,4573,7308,7309],"architecture","voip","BgVCYSOpIlMl2ZrmOYemWo_eFIdT3hIeqxQgEndqHTg",{"id":7312,"title":7313,"author":7,"body":7314,"category":4563,"coverImage":8097,"date":8098,"description":8099,"extension":1027,"meta":8100,"navigation":94,"path":8101,"readingTime":175,"seo":8102,"stem":8103,"tags":8104,"__hash__":8111},"posts\u002Fblog\u002Fvoip-security-sip-hardening.md","VoIP Security: Hardening SIP Infrastructure Against Attacks",{"type":9,"value":7315,"toc":8083},[7316,7319,7322,7326,7329,7378,7382,7392,7395,7401,7411,7415,7421,7425,7431,7437,7440,7444,7447,7486,7546,7561,7565,7568,7647,7653,7657,7660,7731,7735,7738,7743,7787,7792,7845,7850,7884,7888,7891,7950,7953,7957,8078,8081],[12,7317,7313],{"id":7318},"voip-security-hardening-sip-infrastructure-against-attacks",[16,7320,7321],{},"SIP infrastructure is one of the most actively attacked surfaces on the internet. Port 5060 UDP is probed constantly — automated scanners enumerate SIP extensions, brute-force credentials, and launch toll fraud attacks that can generate $10,000–$50,000 in carrier charges before anyone notices. This post covers concrete hardening steps that eliminate the easy attacks and make the hard ones expensive enough to be unprofitable.",[20,7323,7325],{"id":7324},"threat-model","Threat Model",[16,7327,7328],{},"The attacks worth defending against, roughly in order of frequency:",[785,7330,7331,7345,7360,7366,7372],{},[788,7332,7333,7336,7337,7340,7341,7344],{},[2311,7334,7335],{},"Extension enumeration"," — SIPvicious, svwar, and similar tools send OPTIONS or REGISTER requests to every extension from 100 to 9999 looking for valid usernames via ",[28,7338,7339],{},"404 Not Found"," vs ",[28,7342,7343],{},"401 Unauthorized"," responses.",[788,7346,7347,7350,7351,2438,7354,2438,7357,4639],{},[2311,7348,7349],{},"Credential brute-force"," — After finding valid extensions, attackers try common SIP passwords: the extension number itself, ",[28,7352,7353],{},"1234",[28,7355,7356],{},"0000",[28,7358,7359],{},"secret",[788,7361,7362,7365],{},[2311,7363,7364],{},"REGISTER flooding"," — Volumetric attacks that consume CPU processing authentication challenges.",[788,7367,7368,7371],{},[2311,7369,7370],{},"Toll fraud"," — Once authenticated (or by exploiting misconfigured contexts), attackers place calls to premium-rate numbers or international destinations they monetize.",[788,7373,7374,7377],{},[2311,7375,7376],{},"Media injection"," — Inserting RTP packets into established calls to eavesdrop or disrupt.",[20,7379,7381],{"id":7380},"response-code-normalization","Response Code Normalization",[16,7383,7384,7385,7387,7388,7391],{},"The cheapest fix: stop leaking information through SIP response codes. A ",[28,7386,7343],{}," for a valid extension and a ",[28,7389,7390],{},"403 Forbidden"," for an invalid one tells attackers which extensions exist. Return the same response code for both:",[1385,7393,7394],{"id":1036},"Kamailio",[48,7396,7399],{"className":7397,"code":7398,"language":654},[652],"request_route {\n    if (is_method(\"REGISTER\")) {\n        if (!www_authenticate(\"mydomain.com\", \"subscriber\")) {\n            # Always send 401, never 403 or 404\n            www_challenge(\"mydomain.com\", \"1\");\n            exit;\n        }\n        if (!save(\"location\")) {\n            sl_reply_error();\n            exit;\n        }\n        sl_send_reply(\"200\", \"OK\");\n        exit;\n    }\n}\n",[28,7400,7398],{"__ignoreMap":53},[16,7402,7403,7404,7407,7408,7410],{},"Kamailio's ",[28,7405,7406],{},"www_authenticate"," returns false for both wrong password and unknown user. The caller sees only ",[28,7409,7343],{}," either way.",[1385,7412,7414],{"id":7413},"opensips","OpenSIPS",[48,7416,7419],{"className":7417,"code":7418,"language":654},[652],"route {\n    if (is_method(\"REGISTER\")) {\n        if (!www_authorize(\"\", \"subscriber\")) {\n            www_challenge(\"\", 0);\n            exit;\n        }\n        if (!save_contacts(\"location\")) {\n            send_reply(500, \"Internal Error\");\n            exit;\n        }\n    }\n}\n",[28,7420,7418],{"__ignoreMap":53},[20,7422,7424],{"id":7423},"rate-limiting-with-pike-kamailio","Rate Limiting with pike (Kamailio)",[16,7426,629,7427,7430],{},[28,7428,7429],{},"pike"," module tracks per-IP request rates and blocks sources that exceed your threshold:",[48,7432,7435],{"className":7433,"code":7434,"language":654},[652],"loadmodule \"pike.so\"\n\nmodparam(\"pike\", \"sampling_time_unit\", 2)\nmodparam(\"pike\", \"reqs_density_per_unit\", 30)\nmodparam(\"pike\", \"remove_latency\", 4)\n\nrequest_route {\n    if (!pike_check_req()) {\n        xlog(\"L_WARN\", \"Pike blocked $si:$sp - $rm\\n\");\n        exit;\n    }\n    # ... rest of routing\n}\n",[28,7436,7434],{"__ignoreMap":53},[16,7438,7439],{},"This blocks any IP sending more than 30 SIP requests per 2-second window. Legitimate SIP endpoints send a few REGISTER and OPTIONS per minute. An enumeration tool sends hundreds per second.",[20,7441,7443],{"id":7442},"fail2ban-integration","fail2ban Integration",[16,7445,7446],{},"fail2ban reads log files and adds iptables rules to block offending IPs. Configure it to watch Kamailio or Asterisk logs:",[48,7448,7450],{"className":406,"code":7449,"language":408,"meta":53,"style":53},"# \u002Fetc\u002Ffail2ban\u002Ffilter.d\u002Fkamailio.conf\n[Definition]\nfailregex = .*\\[\u003CHOST>\\].*\"(REGISTER|INVITE|OPTIONS)\".*-> 401\n            .*\\[\u003CHOST>\\].*\"(REGISTER|INVITE|OPTIONS)\".*-> 403\n            .*pike blocked.*\\[\u003CHOST>\\]\n\nignoreregex =\n",[28,7451,7452,7457,7462,7467,7472,7477,7481],{"__ignoreMap":53},[57,7453,7454],{"class":59,"line":60},[57,7455,7456],{},"# \u002Fetc\u002Ffail2ban\u002Ffilter.d\u002Fkamailio.conf\n",[57,7458,7459],{"class":59,"line":67},[57,7460,7461],{},"[Definition]\n",[57,7463,7464],{"class":59,"line":81},[57,7465,7466],{},"failregex = .*\\[\u003CHOST>\\].*\"(REGISTER|INVITE|OPTIONS)\".*-> 401\n",[57,7468,7469],{"class":59,"line":91},[57,7470,7471],{},"            .*\\[\u003CHOST>\\].*\"(REGISTER|INVITE|OPTIONS)\".*-> 403\n",[57,7473,7474],{"class":59,"line":98},[57,7475,7476],{},"            .*pike blocked.*\\[\u003CHOST>\\]\n",[57,7478,7479],{"class":59,"line":123},[57,7480,95],{"emptyLinePlaceholder":94},[57,7482,7483],{"class":59,"line":132},[57,7484,7485],{},"ignoreregex =\n",[48,7487,7489],{"className":406,"code":7488,"language":408,"meta":53,"style":53},"# \u002Fetc\u002Ffail2ban\u002Fjail.d\u002Fkamailio.conf\n[kamailio]\nenabled  = true\nport     = 5060,5061\nprotocol = udp\nfilter   = kamailio\nlogpath  = \u002Fvar\u002Flog\u002Fkamailio\u002Fkamailio.log\nmaxretry = 10\nfindtime = 300\nbantime  = 3600\naction   = iptables-allports[name=kamailio, protocol=all]\n",[28,7490,7491,7496,7501,7506,7511,7516,7521,7526,7531,7536,7541],{"__ignoreMap":53},[57,7492,7493],{"class":59,"line":60},[57,7494,7495],{},"# \u002Fetc\u002Ffail2ban\u002Fjail.d\u002Fkamailio.conf\n",[57,7497,7498],{"class":59,"line":67},[57,7499,7500],{},"[kamailio]\n",[57,7502,7503],{"class":59,"line":81},[57,7504,7505],{},"enabled  = true\n",[57,7507,7508],{"class":59,"line":91},[57,7509,7510],{},"port     = 5060,5061\n",[57,7512,7513],{"class":59,"line":98},[57,7514,7515],{},"protocol = udp\n",[57,7517,7518],{"class":59,"line":123},[57,7519,7520],{},"filter   = kamailio\n",[57,7522,7523],{"class":59,"line":132},[57,7524,7525],{},"logpath  = \u002Fvar\u002Flog\u002Fkamailio\u002Fkamailio.log\n",[57,7527,7528],{"class":59,"line":143},[57,7529,7530],{},"maxretry = 10\n",[57,7532,7533],{"class":59,"line":148},[57,7534,7535],{},"findtime = 300\n",[57,7537,7538],{"class":59,"line":154},[57,7539,7540],{},"bantime  = 3600\n",[57,7542,7543],{"class":59,"line":175},[57,7544,7545],{},"action   = iptables-allports[name=kamailio, protocol=all]\n",[16,7547,7548,7549,7552,7553,7556,7557,7560],{},"This bans any IP that triggers 10 auth failures within 5 minutes for 1 hour. Increase ",[28,7550,7551],{},"bantime"," to 86400 (24 hours) for persistent attackers; add a ",[28,7554,7555],{},"[kamailio-repeat]"," jail with ",[28,7558,7559],{},"bantime = -1"," (permanent) for IPs that return after banning.",[20,7562,7564],{"id":7563},"enforcing-tls-for-sip-signaling","Enforcing TLS for SIP Signaling",[16,7566,7567],{},"Plain UDP SIP exposes authentication credentials (MD5 hashed, but crackable offline) and call metadata. Enforce TLS for all external endpoints:",[48,7569,7571],{"className":406,"code":7570,"language":408,"meta":53,"style":53},"# Asterisk PJSIP transport\n[transport-tls]\ntype=transport\nprotocol=tls\nbind=0.0.0.0:5061\ncert_file=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fpbx.example.com\u002Ffullchain.pem\npriv_key_file=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fpbx.example.com\u002Fprivkey.pem\ncipher=ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384\nmethod=tlsv1_2\n\n[endpoint-tls-template]\ntype=endpoint\ntransport=transport-tls\nmedia_encryption=sdes\nmedia_encryption_optimistic=no\n",[28,7572,7573,7578,7583,7588,7593,7598,7603,7608,7613,7618,7622,7627,7632,7637,7642],{"__ignoreMap":53},[57,7574,7575],{"class":59,"line":60},[57,7576,7577],{},"# Asterisk PJSIP transport\n",[57,7579,7580],{"class":59,"line":67},[57,7581,7582],{},"[transport-tls]\n",[57,7584,7585],{"class":59,"line":81},[57,7586,7587],{},"type=transport\n",[57,7589,7590],{"class":59,"line":91},[57,7591,7592],{},"protocol=tls\n",[57,7594,7595],{"class":59,"line":98},[57,7596,7597],{},"bind=0.0.0.0:5061\n",[57,7599,7600],{"class":59,"line":123},[57,7601,7602],{},"cert_file=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fpbx.example.com\u002Ffullchain.pem\n",[57,7604,7605],{"class":59,"line":132},[57,7606,7607],{},"priv_key_file=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fpbx.example.com\u002Fprivkey.pem\n",[57,7609,7610],{"class":59,"line":143},[57,7611,7612],{},"cipher=ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384\n",[57,7614,7615],{"class":59,"line":148},[57,7616,7617],{},"method=tlsv1_2\n",[57,7619,7620],{"class":59,"line":154},[57,7621,95],{"emptyLinePlaceholder":94},[57,7623,7624],{"class":59,"line":175},[57,7625,7626],{},"[endpoint-tls-template]\n",[57,7628,7629],{"class":59,"line":190},[57,7630,7631],{},"type=endpoint\n",[57,7633,7634],{"class":59,"line":195},[57,7635,7636],{},"transport=transport-tls\n",[57,7638,7639],{"class":59,"line":207},[57,7640,7641],{},"media_encryption=sdes\n",[57,7643,7644],{"class":59,"line":216},[57,7645,7646],{},"media_encryption_optimistic=no\n",[16,7648,5163,7649,7652],{},[28,7650,7651],{},"media_encryption_optimistic=no"," forces SRTP — calls fail rather than fall back to cleartext RTP. This is the right default for anything carrying confidential calls.",[20,7654,7656],{"id":7655},"srtp-for-media","SRTP for Media",[16,7658,7659],{},"SIP over TLS protects signaling. SRTP protects the media. Enabling both closes the full eavesdropping path:",[48,7661,7663],{"className":406,"code":7662,"language":408,"meta":53,"style":53},"# Kamailio: enforce SRTP by rejecting offers without crypto\nrequest_route {\n    if (is_method(\"INVITE\")) {\n        if (!has_body(\"application\u002Fsdp\")) {\n            sl_send_reply(\"400\", \"Bad Request\");\n            exit;\n        }\n        # Check for SRTP crypto line in SDP\n        if (!search_body(\"a=crypto:\")) {\n            sl_send_reply(\"488\", \"Not Acceptable Here\");\n            exit;\n        }\n    }\n}\n",[28,7664,7665,7670,7675,7680,7685,7690,7695,7700,7705,7710,7715,7719,7723,7727],{"__ignoreMap":53},[57,7666,7667],{"class":59,"line":60},[57,7668,7669],{},"# Kamailio: enforce SRTP by rejecting offers without crypto\n",[57,7671,7672],{"class":59,"line":67},[57,7673,7674],{},"request_route {\n",[57,7676,7677],{"class":59,"line":81},[57,7678,7679],{},"    if (is_method(\"INVITE\")) {\n",[57,7681,7682],{"class":59,"line":91},[57,7683,7684],{},"        if (!has_body(\"application\u002Fsdp\")) {\n",[57,7686,7687],{"class":59,"line":98},[57,7688,7689],{},"            sl_send_reply(\"400\", \"Bad Request\");\n",[57,7691,7692],{"class":59,"line":123},[57,7693,7694],{},"            exit;\n",[57,7696,7697],{"class":59,"line":132},[57,7698,7699],{},"        }\n",[57,7701,7702],{"class":59,"line":143},[57,7703,7704],{},"        # Check for SRTP crypto line in SDP\n",[57,7706,7707],{"class":59,"line":148},[57,7708,7709],{},"        if (!search_body(\"a=crypto:\")) {\n",[57,7711,7712],{"class":59,"line":154},[57,7713,7714],{},"            sl_send_reply(\"488\", \"Not Acceptable Here\");\n",[57,7716,7717],{"class":59,"line":175},[57,7718,7694],{},[57,7720,7721],{"class":59,"line":190},[57,7722,7699],{},[57,7724,7725],{"class":59,"line":195},[57,7726,4057],{},[57,7728,7729],{"class":59,"line":207},[57,7730,3810],{},[20,7732,7734],{"id":7733},"dialplan-fraud-prevention","Dialplan Fraud Prevention",[16,7736,7737],{},"Toll fraud usually exploits a misconfigured dialplan context. Common mistakes:",[16,7739,7740],{},[2311,7741,7742],{},"Never allow unauthenticated access to outbound routes:",[48,7744,7746],{"className":406,"code":7745,"language":408,"meta":53,"style":53},"# WRONG — any SIP device that reaches this context can dial out\n[default]\nexten => _9.,1,Dial(PJSIP\u002F${EXTEN:1}@carrier)\n\n# RIGHT — only authenticated extensions reach from-internal\n[from-internal]\nexten => _9.,1,GoSub(sub-check-credit,s,1)\n same => n,Dial(PJSIP\u002F${EXTEN:1}@carrier)\n",[28,7747,7748,7753,7758,7763,7767,7772,7777,7782],{"__ignoreMap":53},[57,7749,7750],{"class":59,"line":60},[57,7751,7752],{},"# WRONG — any SIP device that reaches this context can dial out\n",[57,7754,7755],{"class":59,"line":67},[57,7756,7757],{},"[default]\n",[57,7759,7760],{"class":59,"line":81},[57,7761,7762],{},"exten => _9.,1,Dial(PJSIP\u002F${EXTEN:1}@carrier)\n",[57,7764,7765],{"class":59,"line":91},[57,7766,95],{"emptyLinePlaceholder":94},[57,7768,7769],{"class":59,"line":98},[57,7770,7771],{},"# RIGHT — only authenticated extensions reach from-internal\n",[57,7773,7774],{"class":59,"line":123},[57,7775,7776],{},"[from-internal]\n",[57,7778,7779],{"class":59,"line":132},[57,7780,7781],{},"exten => _9.,1,GoSub(sub-check-credit,s,1)\n",[57,7783,7784],{"class":59,"line":143},[57,7785,7786],{}," same => n,Dial(PJSIP\u002F${EXTEN:1}@carrier)\n",[16,7788,7789],{},[2311,7790,7791],{},"Restrict international dialing by default:",[48,7793,7795],{"className":406,"code":7794,"language":408,"meta":53,"style":53},"[from-internal]\n; Domestic only by default\nexten => _NXXNXXXXXX,1,GoSub(sub-dialout,s,1(${EXTEN}))\n\n; International requires explicit permission via DB lookup\nexten => _011.,1,GoSub(sub-check-international-permission,s,1(${CALLERID(num)}))\n same => n,GotoIf($[\"${PERMISSION}\" = \"yes\"]?allowed:denied)\n same => n(allowed),GoSub(sub-dialout,s,1(${EXTEN}))\n same => n(denied),Playback(ss-noservice)\n same => n,Hangup()\n",[28,7796,7797,7801,7806,7811,7815,7820,7825,7830,7835,7840],{"__ignoreMap":53},[57,7798,7799],{"class":59,"line":60},[57,7800,7776],{},[57,7802,7803],{"class":59,"line":67},[57,7804,7805],{},"; Domestic only by default\n",[57,7807,7808],{"class":59,"line":81},[57,7809,7810],{},"exten => _NXXNXXXXXX,1,GoSub(sub-dialout,s,1(${EXTEN}))\n",[57,7812,7813],{"class":59,"line":91},[57,7814,95],{"emptyLinePlaceholder":94},[57,7816,7817],{"class":59,"line":98},[57,7818,7819],{},"; International requires explicit permission via DB lookup\n",[57,7821,7822],{"class":59,"line":123},[57,7823,7824],{},"exten => _011.,1,GoSub(sub-check-international-permission,s,1(${CALLERID(num)}))\n",[57,7826,7827],{"class":59,"line":132},[57,7828,7829],{}," same => n,GotoIf($[\"${PERMISSION}\" = \"yes\"]?allowed:denied)\n",[57,7831,7832],{"class":59,"line":143},[57,7833,7834],{}," same => n(allowed),GoSub(sub-dialout,s,1(${EXTEN}))\n",[57,7836,7837],{"class":59,"line":148},[57,7838,7839],{}," same => n(denied),Playback(ss-noservice)\n",[57,7841,7842],{"class":59,"line":154},[57,7843,7844],{}," same => n,Hangup()\n",[16,7846,7847],{},[2311,7848,7849],{},"Set per-account call limits:",[48,7851,7853],{"className":406,"code":7852,"language":408,"meta":53,"style":53},"[sub-check-credit]\nexten => s,1,Set(ACTIVE_CALLS=${DB(calls\u002F${CALLERID(num)}\u002Factive)})\n same => n,GotoIf($[${ACTIVE_CALLS} >= 3]?overlimit)\n same => n,Return()\n same => n(overlimit),Playback(ss-noservice)\n same => n,Hangup()\n",[28,7854,7855,7860,7865,7870,7875,7880],{"__ignoreMap":53},[57,7856,7857],{"class":59,"line":60},[57,7858,7859],{},"[sub-check-credit]\n",[57,7861,7862],{"class":59,"line":67},[57,7863,7864],{},"exten => s,1,Set(ACTIVE_CALLS=${DB(calls\u002F${CALLERID(num)}\u002Factive)})\n",[57,7866,7867],{"class":59,"line":81},[57,7868,7869],{}," same => n,GotoIf($[${ACTIVE_CALLS} >= 3]?overlimit)\n",[57,7871,7872],{"class":59,"line":91},[57,7873,7874],{}," same => n,Return()\n",[57,7876,7877],{"class":59,"line":98},[57,7878,7879],{}," same => n(overlimit),Playback(ss-noservice)\n",[57,7881,7882],{"class":59,"line":123},[57,7883,7844],{},[20,7885,7887],{"id":7886},"anomaly-detection-spend-velocity-alerts","Anomaly Detection: Spend Velocity Alerts",[16,7889,7890],{},"Technical controls stop known attack patterns. Behavioral anomaly detection catches novel attacks. Track per-account spend velocity against your carrier CDR feed:",[48,7892,7894],{"className":5279,"code":7893,"language":5281,"meta":53,"style":53},"-- Alert query: accounts exceeding $50 in the last hour\nSELECT\n    account_id,\n    SUM(duration_seconds * rate_per_second) AS spend_last_hour,\n    COUNT(*) AS call_count\nFROM cdr\nWHERE call_start > NOW() - INTERVAL '1 hour'\n  AND call_type = 'outbound'\nGROUP BY account_id\nHAVING SUM(duration_seconds * rate_per_second) > 50\nORDER BY spend_last_hour DESC;\n",[28,7895,7896,7901,7905,7910,7915,7920,7925,7930,7935,7940,7945],{"__ignoreMap":53},[57,7897,7898],{"class":59,"line":60},[57,7899,7900],{},"-- Alert query: accounts exceeding $50 in the last hour\n",[57,7902,7903],{"class":59,"line":67},[57,7904,5288],{},[57,7906,7907],{"class":59,"line":81},[57,7908,7909],{},"    account_id,\n",[57,7911,7912],{"class":59,"line":91},[57,7913,7914],{},"    SUM(duration_seconds * rate_per_second) AS spend_last_hour,\n",[57,7916,7917],{"class":59,"line":98},[57,7918,7919],{},"    COUNT(*) AS call_count\n",[57,7921,7922],{"class":59,"line":123},[57,7923,7924],{},"FROM cdr\n",[57,7926,7927],{"class":59,"line":132},[57,7928,7929],{},"WHERE call_start > NOW() - INTERVAL '1 hour'\n",[57,7931,7932],{"class":59,"line":143},[57,7933,7934],{},"  AND call_type = 'outbound'\n",[57,7936,7937],{"class":59,"line":148},[57,7938,7939],{},"GROUP BY account_id\n",[57,7941,7942],{"class":59,"line":154},[57,7943,7944],{},"HAVING SUM(duration_seconds * rate_per_second) > 50\n",[57,7946,7947],{"class":59,"line":175},[57,7948,7949],{},"ORDER BY spend_last_hour DESC;\n",[16,7951,7952],{},"Run this query every 5 minutes from a monitoring job. When an account crosses the threshold, suspend outbound calling automatically and page the on-call team. The 5-minute detection window limits maximum fraud exposure to roughly $50 × (response time \u002F 5 minutes).",[20,7954,7956],{"id":7955},"security-hardening-checklist","Security Hardening Checklist",[661,7958,7959,7972],{},[664,7960,7961],{},[667,7962,7963,7966,7969],{},[670,7964,7965],{},"Control",[670,7967,7968],{},"Priority",[670,7970,7971],{},"Implementation",[677,7973,7974,7985,7995,8005,8015,8025,8038,8048,8058,8068],{},[667,7975,7976,7979,7982],{},[682,7977,7978],{},"Response code normalization",[682,7980,7981],{},"Critical",[682,7983,7984],{},"Kamailio\u002FOpenSIPS routing block",[667,7986,7987,7990,7992],{},[682,7988,7989],{},"Rate limiting (pike \u002F ratelimit)",[682,7991,7981],{},[682,7993,7994],{},"Load pike module",[667,7996,7997,8000,8002],{},[682,7998,7999],{},"fail2ban on auth failures",[682,8001,7981],{},[682,8003,8004],{},"fail2ban + iptables",[667,8006,8007,8010,8012],{},[682,8008,8009],{},"TLS for SIP signaling",[682,8011,3472],{},[682,8013,8014],{},"PJSIP TLS transport",[667,8016,8017,8020,8022],{},[682,8018,8019],{},"SRTP for media",[682,8021,3472],{},[682,8023,8024],{},"PJSIP + rtpengine SRTP",[667,8026,8027,8030,8032],{},[682,8028,8029],{},"Deny RFC 1918 relay in TURN",[682,8031,3472],{},[682,8033,8034,8035],{},"coturn ",[28,8036,8037],{},"denied-peer-ip",[667,8039,8040,8043,8045],{},[682,8041,8042],{},"International dialing restrictions",[682,8044,3472],{},[682,8046,8047],{},"Dialplan permission check",[667,8049,8050,8053,8055],{},[682,8051,8052],{},"Per-account call limits",[682,8054,3458],{},[682,8056,8057],{},"DB-backed call counter",[667,8059,8060,8063,8065],{},[682,8061,8062],{},"Spend velocity alerting",[682,8064,3458],{},[682,8066,8067],{},"CDR monitoring query",[667,8069,8070,8073,8075],{},[682,8071,8072],{},"Homer SIPcapture for forensics",[682,8074,3458],{},[682,8076,8077],{},"sipcapture module",[16,8079,8080],{},"A SIP infrastructure that passes this checklist is not unattackable — determined, well-funded attackers still exist. But it is unprofitable to attack opportunistically, which eliminates 95% of the actual threat volume you'll see in production.",[1009,8082,3478],{},{"title":53,"searchDepth":67,"depth":67,"links":8084},[8085,8086,8090,8091,8092,8093,8094,8095,8096],{"id":7324,"depth":67,"text":7325},{"id":7380,"depth":67,"text":7381,"children":8087},[8088,8089],{"id":1036,"depth":81,"text":7394},{"id":7413,"depth":81,"text":7414},{"id":7423,"depth":67,"text":7424},{"id":7442,"depth":67,"text":7443},{"id":7563,"depth":67,"text":7564},{"id":7655,"depth":67,"text":7656},{"id":7733,"depth":67,"text":7734},{"id":7886,"depth":67,"text":7887},{"id":7955,"depth":67,"text":7956},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555949963-aa79dcee981c?w=1200&q=80","2025-09-01","Practical SIP security hardening: authentication brute-force prevention, REGISTER flooding mitigation, TLS enforcement, fraud detection patterns, and fail2ban configuration.",{},"\u002Fblog\u002Fvoip-security-sip-hardening",{"title":7313,"description":8099},"blog\u002Fvoip-security-sip-hardening",[8105,8106,8107,8108,8109,8110],"voip-security","sip","hardening","fraud","fail2ban","authentication","ne4Qc017dIvP9S76liXjYw25jhBHhM5KBhKSfY0Wxjk",{"id":8113,"title":8114,"author":7,"body":8115,"category":3495,"coverImage":7300,"date":9155,"description":9156,"extension":1027,"meta":9157,"navigation":94,"path":9158,"readingTime":143,"seo":9159,"stem":9160,"tags":9161,"__hash__":9166},"posts\u002Fblog\u002Fwebrtc-turn-server-deployment.md","Deploying a Production TURN Server: coturn Configuration Guide",{"type":9,"value":8116,"toc":9144},[8117,8120,8123,8127,8130,8150,8153,8157,8160,8229,8232,8236,8278,8282,8482,8487,8491,8494,8543,8546,8610,8613,8628,8631,8635,8746,8753,8757,8763,8766,8835,8972,8976,8979,8982,9071,9074,9078,9081,9131,9141],[12,8118,8114],{"id":8119},"deploying-a-production-turn-server-coturn-configuration-guide",[16,8121,8122],{},"A TURN server is the piece most WebRTC deployments underestimate until users behind symmetric NAT or enterprise firewalls start reporting failed calls. STUN handles roughly 80% of NAT traversal cases. The remaining 20% — corporate networks with strict egress filtering, CGNAT carriers, mobile networks with aggressive NAT — require a TURN relay. coturn is the de-facto open-source TURN implementation. This guide covers a production-grade deployment, not the five-line \"it works on localhost\" setup.",[20,8124,8126],{"id":8125},"how-turn-fits-into-webrtc-ice","How TURN Fits Into WebRTC ICE",[16,8128,8129],{},"When two WebRTC peers connect, the ICE (Interactive Connectivity Establishment) process collects candidate addresses from three sources:",[785,8131,8132,8138,8144],{},[788,8133,8134,8137],{},[2311,8135,8136],{},"Host candidates"," — the interface's own IP addresses",[788,8139,8140,8143],{},[2311,8141,8142],{},"Server-reflexive candidates"," — the public IP seen by a STUN server",[788,8145,8146,8149],{},[2311,8147,8148],{},"Relay candidates"," — IP:port pairs allocated on the TURN server",[16,8151,8152],{},"Relay candidates are the fallback. Media flows through the TURN server instead of peer-to-peer, adding latency (typically 20–60ms round-trip overhead) but guaranteeing connectivity. A TURN server handles both UDP and TCP relay, plus TLS-wrapped TCP (TURNS) for environments that block non-HTTP traffic.",[20,8154,8156],{"id":8155},"hardware-sizing","Hardware Sizing",[16,8158,8159],{},"TURN relay is bandwidth-bound, not compute-bound. Each relayed call uses two UDP streams at the TURN server.",[661,8161,8162,8178],{},[664,8163,8164],{},[667,8165,8166,8169,8172,8175],{},[670,8167,8168],{},"Concurrent relayed calls",[670,8170,8171],{},"Bitrate per call",[670,8173,8174],{},"Required throughput",[670,8176,8177],{},"Recommended NIC",[677,8179,8180,8193,8204,8216],{},[667,8181,8182,8184,8187,8190],{},[682,8183,6882],{},[682,8185,8186],{},"100 Kbps audio",[682,8188,8189],{},"20 Mbps",[682,8191,8192],{},"100 Mbps",[667,8194,8195,8197,8199,8201],{},[682,8196,6868],{},[682,8198,8186],{},[682,8200,8192],{},[682,8202,8203],{},"1 Gbps",[667,8205,8206,8208,8211,8213],{},[682,8207,6871],{},[682,8209,8210],{},"500 Kbps video",[682,8212,8203],{},[682,8214,8215],{},"10 Gbps",[667,8217,8218,8221,8223,8226],{},[682,8219,8220],{},"5,000",[682,8222,8210],{},[682,8224,8225],{},"5 Gbps",[682,8227,8228],{},"10 Gbps bonded",[16,8230,8231],{},"CPU usage is negligible — coturn uses minimal processing per relay allocation. Memory is ~4 KB per active allocation. A $40\u002Fmonth VPS with a 1 Gbps NIC handles 500 concurrent relayed calls comfortably.",[20,8233,8235],{"id":8234},"installing-coturn","Installing coturn",[48,8237,8239],{"className":50,"code":8238,"language":52,"meta":53,"style":53},"# Debian \u002F Ubuntu\napt-get install coturn\n\n# Enable the service\nsed -i 's\u002F#TURNSERVER_ENABLED=1\u002FTURNSERVER_ENABLED=1\u002F' \u002Fetc\u002Fdefault\u002Fcoturn\n",[28,8240,8241,8246,8255,8259,8264],{"__ignoreMap":53},[57,8242,8243],{"class":59,"line":60},[57,8244,8245],{"class":63},"# Debian \u002F Ubuntu\n",[57,8247,8248,8250,8252],{"class":59,"line":67},[57,8249,126],{"class":101},[57,8251,137],{"class":74},[57,8253,8254],{"class":74}," coturn\n",[57,8256,8257],{"class":59,"line":81},[57,8258,95],{"emptyLinePlaceholder":94},[57,8260,8261],{"class":59,"line":91},[57,8262,8263],{"class":63},"# Enable the service\n",[57,8265,8266,8269,8272,8275],{"class":59,"line":98},[57,8267,8268],{"class":101},"sed",[57,8270,8271],{"class":70}," -i",[57,8273,8274],{"class":74}," 's\u002F#TURNSERVER_ENABLED=1\u002FTURNSERVER_ENABLED=1\u002F'",[57,8276,8277],{"class":74}," \u002Fetc\u002Fdefault\u002Fcoturn\n",[20,8279,8281],{"id":8280},"core-configuration-etcturnserverconf","Core Configuration (\u002Fetc\u002Fturnserver.conf)",[48,8283,8285],{"className":406,"code":8284,"language":408,"meta":53,"style":53},"# Network\nlistening-port=3478\ntls-listening-port=5349\nlistening-ip=0.0.0.0\nrelay-ip=YOUR_PUBLIC_IP\nexternal-ip=YOUR_PUBLIC_IP\n\n# Authentication — use long-term credential mechanism\nlt-cred-mech\nrealm=turn.example.com\n\n# TLS — use a real cert, not self-signed\ncert=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fturn.example.com\u002Ffullchain.pem\npkey=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fturn.example.com\u002Fprivkey.pem\ncipher-list=\"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256\"\nno-sslv3\nno-tlsv1\nno-tlsv1_1\n\n# Database for credentials\nuserdb=\u002Fvar\u002Flib\u002Fturn\u002Fturndb\n\n# Logging\nlog-file=\u002Fvar\u002Flog\u002Fcoturn\u002Fturnserver.log\nverbose\n\n# Security hardening\nno-loopback-peers\nno-multicast-peers\ndenied-peer-ip=10.0.0.0-10.255.255.255\ndenied-peer-ip=192.168.0.0-192.168.255.255\ndenied-peer-ip=172.16.0.0-172.31.255.255\n\n# Quota enforcement\nuser-quota=10\ntotal-quota=1000\nmax-bps=500000\n\n# Prometheus metrics\nprometheus\nprometheus-port=9641\n",[28,8286,8287,8291,8296,8301,8306,8311,8316,8320,8325,8330,8335,8339,8344,8349,8354,8359,8364,8369,8374,8378,8383,8388,8392,8396,8401,8406,8410,8415,8420,8425,8430,8435,8440,8444,8449,8454,8459,8464,8468,8473,8477],{"__ignoreMap":53},[57,8288,8289],{"class":59,"line":60},[57,8290,425],{},[57,8292,8293],{"class":59,"line":67},[57,8294,8295],{},"listening-port=3478\n",[57,8297,8298],{"class":59,"line":81},[57,8299,8300],{},"tls-listening-port=5349\n",[57,8302,8303],{"class":59,"line":91},[57,8304,8305],{},"listening-ip=0.0.0.0\n",[57,8307,8308],{"class":59,"line":98},[57,8309,8310],{},"relay-ip=YOUR_PUBLIC_IP\n",[57,8312,8313],{"class":59,"line":123},[57,8314,8315],{},"external-ip=YOUR_PUBLIC_IP\n",[57,8317,8318],{"class":59,"line":132},[57,8319,95],{"emptyLinePlaceholder":94},[57,8321,8322],{"class":59,"line":143},[57,8323,8324],{},"# Authentication — use long-term credential mechanism\n",[57,8326,8327],{"class":59,"line":148},[57,8328,8329],{},"lt-cred-mech\n",[57,8331,8332],{"class":59,"line":154},[57,8333,8334],{},"realm=turn.example.com\n",[57,8336,8337],{"class":59,"line":175},[57,8338,95],{"emptyLinePlaceholder":94},[57,8340,8341],{"class":59,"line":190},[57,8342,8343],{},"# TLS — use a real cert, not self-signed\n",[57,8345,8346],{"class":59,"line":195},[57,8347,8348],{},"cert=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fturn.example.com\u002Ffullchain.pem\n",[57,8350,8351],{"class":59,"line":207},[57,8352,8353],{},"pkey=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fturn.example.com\u002Fprivkey.pem\n",[57,8355,8356],{"class":59,"line":216},[57,8357,8358],{},"cipher-list=\"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256\"\n",[57,8360,8361],{"class":59,"line":222},[57,8362,8363],{},"no-sslv3\n",[57,8365,8366],{"class":59,"line":490},[57,8367,8368],{},"no-tlsv1\n",[57,8370,8371],{"class":59,"line":496},[57,8372,8373],{},"no-tlsv1_1\n",[57,8375,8376],{"class":59,"line":502},[57,8377,95],{"emptyLinePlaceholder":94},[57,8379,8380],{"class":59,"line":507},[57,8381,8382],{},"# Database for credentials\n",[57,8384,8385],{"class":59,"line":513},[57,8386,8387],{},"userdb=\u002Fvar\u002Flib\u002Fturn\u002Fturndb\n",[57,8389,8390],{"class":59,"line":519},[57,8391,95],{"emptyLinePlaceholder":94},[57,8393,8394],{"class":59,"line":525},[57,8395,510],{},[57,8397,8398],{"class":59,"line":531},[57,8399,8400],{},"log-file=\u002Fvar\u002Flog\u002Fcoturn\u002Fturnserver.log\n",[57,8402,8403],{"class":59,"line":536},[57,8404,8405],{},"verbose\n",[57,8407,8408],{"class":59,"line":542},[57,8409,95],{"emptyLinePlaceholder":94},[57,8411,8412],{"class":59,"line":548},[57,8413,8414],{},"# Security hardening\n",[57,8416,8417],{"class":59,"line":554},[57,8418,8419],{},"no-loopback-peers\n",[57,8421,8422],{"class":59,"line":560},[57,8423,8424],{},"no-multicast-peers\n",[57,8426,8427],{"class":59,"line":565},[57,8428,8429],{},"denied-peer-ip=10.0.0.0-10.255.255.255\n",[57,8431,8432],{"class":59,"line":571},[57,8433,8434],{},"denied-peer-ip=192.168.0.0-192.168.255.255\n",[57,8436,8437],{"class":59,"line":577},[57,8438,8439],{},"denied-peer-ip=172.16.0.0-172.31.255.255\n",[57,8441,8442],{"class":59,"line":582},[57,8443,95],{"emptyLinePlaceholder":94},[57,8445,8446],{"class":59,"line":588},[57,8447,8448],{},"# Quota enforcement\n",[57,8450,8451],{"class":59,"line":594},[57,8452,8453],{},"user-quota=10\n",[57,8455,8456],{"class":59,"line":600},[57,8457,8458],{},"total-quota=1000\n",[57,8460,8461],{"class":59,"line":605},[57,8462,8463],{},"max-bps=500000\n",[57,8465,8466],{"class":59,"line":611},[57,8467,95],{"emptyLinePlaceholder":94},[57,8469,8470],{"class":59,"line":617},[57,8471,8472],{},"# Prometheus metrics\n",[57,8474,8475],{"class":59,"line":623},[57,8476,1435],{},[57,8478,8479],{"class":59,"line":2155},[57,8480,8481],{},"prometheus-port=9641\n",[16,8483,629,8484,8486],{},[28,8485,8037],{}," directives are critical for security. Without them, an attacker can use your TURN server to relay traffic to internal RFC 1918 addresses — effectively using it as a proxy to reach your private network. Always block private ranges.",[20,8488,8490],{"id":8489},"credential-management","Credential Management",[16,8492,8493],{},"coturn uses SQLite by default. For multi-server deployments, switch to PostgreSQL or Redis so all nodes share the same credential store.",[48,8495,8497],{"className":50,"code":8496,"language":52,"meta":53,"style":53},"# Add a static user (for testing only)\nturnadmin -a -u testuser -r turn.example.com -p secretpass\n\n# Generate a time-limited credential (for production)\n# HMAC-SHA1 of \"timestamp:username\" with your shared secret\n",[28,8498,8499,8504,8529,8533,8538],{"__ignoreMap":53},[57,8500,8501],{"class":59,"line":60},[57,8502,8503],{"class":63},"# Add a static user (for testing only)\n",[57,8505,8506,8509,8512,8515,8518,8520,8523,8526],{"class":59,"line":67},[57,8507,8508],{"class":101},"turnadmin",[57,8510,8511],{"class":70}," -a",[57,8513,8514],{"class":70}," -u",[57,8516,8517],{"class":74}," testuser",[57,8519,261],{"class":70},[57,8521,8522],{"class":74}," turn.example.com",[57,8524,8525],{"class":70}," -p",[57,8527,8528],{"class":74}," secretpass\n",[57,8530,8531],{"class":59,"line":81},[57,8532,95],{"emptyLinePlaceholder":94},[57,8534,8535],{"class":59,"line":91},[57,8536,8537],{"class":63},"# Generate a time-limited credential (for production)\n",[57,8539,8540],{"class":59,"line":98},[57,8541,8542],{"class":63},"# HMAC-SHA1 of \"timestamp:username\" with your shared secret\n",[16,8544,8545],{},"For production WebRTC applications, use the REST API credential pattern. Your application server generates short-lived TURN credentials on demand:",[48,8547,8549],{"className":1536,"code":8548,"language":1538,"meta":53,"style":53},"import hmac, hashlib, base64, time\n\ndef generate_turn_credentials(username, secret, ttl=3600):\n    timestamp = int(time.time()) + ttl\n    turn_user = f\"{timestamp}:{username}\"\n    dig = hmac.new(\n        secret.encode(),\n        turn_user.encode(),\n        hashlib.sha1\n    ).digest()\n    password = base64.b64encode(dig).decode()\n    return {\"username\": turn_user, \"password\": password}\n",[28,8550,8551,8556,8560,8565,8570,8575,8580,8585,8590,8595,8600,8605],{"__ignoreMap":53},[57,8552,8553],{"class":59,"line":60},[57,8554,8555],{},"import hmac, hashlib, base64, time\n",[57,8557,8558],{"class":59,"line":67},[57,8559,95],{"emptyLinePlaceholder":94},[57,8561,8562],{"class":59,"line":81},[57,8563,8564],{},"def generate_turn_credentials(username, secret, ttl=3600):\n",[57,8566,8567],{"class":59,"line":91},[57,8568,8569],{},"    timestamp = int(time.time()) + ttl\n",[57,8571,8572],{"class":59,"line":98},[57,8573,8574],{},"    turn_user = f\"{timestamp}:{username}\"\n",[57,8576,8577],{"class":59,"line":123},[57,8578,8579],{},"    dig = hmac.new(\n",[57,8581,8582],{"class":59,"line":132},[57,8583,8584],{},"        secret.encode(),\n",[57,8586,8587],{"class":59,"line":143},[57,8588,8589],{},"        turn_user.encode(),\n",[57,8591,8592],{"class":59,"line":148},[57,8593,8594],{},"        hashlib.sha1\n",[57,8596,8597],{"class":59,"line":154},[57,8598,8599],{},"    ).digest()\n",[57,8601,8602],{"class":59,"line":175},[57,8603,8604],{},"    password = base64.b64encode(dig).decode()\n",[57,8606,8607],{"class":59,"line":190},[57,8608,8609],{},"    return {\"username\": turn_user, \"password\": password}\n",[16,8611,8612],{},"Configure coturn to validate these credentials:",[48,8614,8616],{"className":406,"code":8615,"language":408,"meta":53,"style":53},"use-auth-secret\nstatic-auth-secret=YOUR_SHARED_SECRET\n",[28,8617,8618,8623],{"__ignoreMap":53},[57,8619,8620],{"class":59,"line":60},[57,8621,8622],{},"use-auth-secret\n",[57,8624,8625],{"class":59,"line":67},[57,8626,8627],{},"static-auth-secret=YOUR_SHARED_SECRET\n",[16,8629,8630],{},"This way, TURN credentials expire automatically (the timestamp is baked in) and you never store user passwords in the TURN database. A compromised TURN credential is useless after TTL seconds.",[20,8632,8634],{"id":8633},"firewall-rules","Firewall Rules",[48,8636,8638],{"className":50,"code":8637,"language":52,"meta":53,"style":53},"# Required ports\nufw allow 3478\u002Fudp   # STUN\u002FTURN\nufw allow 3478\u002Ftcp   # TURN over TCP\nufw allow 5349\u002Ftcp   # TURNS (TLS)\nufw allow 5349\u002Fudp   # TURNS (DTLS)\n\n# Relay port range — must match coturn config\nufw allow 49152:65535\u002Fudp\n\n# Prometheus scrape (from monitoring server only)\nufw allow from MONITOR_IP to any port 9641\n",[28,8639,8640,8645,8659,8671,8683,8695,8699,8704,8713,8717,8722],{"__ignoreMap":53},[57,8641,8642],{"class":59,"line":60},[57,8643,8644],{"class":63},"# Required ports\n",[57,8646,8647,8650,8653,8656],{"class":59,"line":67},[57,8648,8649],{"class":101},"ufw",[57,8651,8652],{"class":74}," allow",[57,8654,8655],{"class":74}," 3478\u002Fudp",[57,8657,8658],{"class":63},"   # STUN\u002FTURN\n",[57,8660,8661,8663,8665,8668],{"class":59,"line":81},[57,8662,8649],{"class":101},[57,8664,8652],{"class":74},[57,8666,8667],{"class":74}," 3478\u002Ftcp",[57,8669,8670],{"class":63},"   # TURN over TCP\n",[57,8672,8673,8675,8677,8680],{"class":59,"line":91},[57,8674,8649],{"class":101},[57,8676,8652],{"class":74},[57,8678,8679],{"class":74}," 5349\u002Ftcp",[57,8681,8682],{"class":63},"   # TURNS (TLS)\n",[57,8684,8685,8687,8689,8692],{"class":59,"line":98},[57,8686,8649],{"class":101},[57,8688,8652],{"class":74},[57,8690,8691],{"class":74}," 5349\u002Fudp",[57,8693,8694],{"class":63},"   # TURNS (DTLS)\n",[57,8696,8697],{"class":59,"line":123},[57,8698,95],{"emptyLinePlaceholder":94},[57,8700,8701],{"class":59,"line":132},[57,8702,8703],{"class":63},"# Relay port range — must match coturn config\n",[57,8705,8706,8708,8710],{"class":59,"line":143},[57,8707,8649],{"class":101},[57,8709,8652],{"class":74},[57,8711,8712],{"class":74}," 49152:65535\u002Fudp\n",[57,8714,8715],{"class":59,"line":148},[57,8716,95],{"emptyLinePlaceholder":94},[57,8718,8719],{"class":59,"line":154},[57,8720,8721],{"class":63},"# Prometheus scrape (from monitoring server only)\n",[57,8723,8724,8726,8728,8731,8734,8737,8740,8743],{"class":59,"line":175},[57,8725,8649],{"class":101},[57,8727,8652],{"class":74},[57,8729,8730],{"class":74}," from",[57,8732,8733],{"class":74}," MONITOR_IP",[57,8735,8736],{"class":74}," to",[57,8738,8739],{"class":74}," any",[57,8741,8742],{"class":74}," port",[57,8744,8745],{"class":70}," 9641\n",[16,8747,8748,8749,8752],{},"The relay port range (49152–65535) is where coturn allocates relay endpoints. Each active TURN allocation uses one port from this range. Size the range to at least ",[28,8750,8751],{},"total-quota * 2"," ports.",[20,8754,8756],{"id":8755},"prometheus-metrics-and-alerting","Prometheus Metrics and Alerting",[16,8758,8759,8760,8762],{},"coturn exposes Prometheus metrics on port 9641 when ",[28,8761,2703],{}," is set in the config.",[16,8764,8765],{},"Key metrics to alert on:",[661,8767,8768,8778],{},[664,8769,8770],{},[667,8771,8772,8774,8776],{},[670,8773,1750],{},[670,8775,1753],{},[670,8777,3557],{},[677,8779,8780,8793,8809,8822],{},[667,8781,8782,8787,8790],{},[682,8783,8784],{},[28,8785,8786],{},"coturn_total_allocations_quota_exceeded_total",[682,8788,8789],{},"> 0 per minute",[682,8791,8792],{},"Users hitting quota limits",[667,8794,8795,8800,8806],{},[682,8796,8797],{},[28,8798,8799],{},"coturn_current_allocations",[682,8801,8802,8803],{},"> 80% of ",[28,8804,8805],{},"total-quota",[682,8807,8808],{},"Approaching capacity",[667,8810,8811,8816,8819],{},[682,8812,8813],{},[28,8814,8815],{},"coturn_total_traffic_bytes",[682,8817,8818],{},"Rate > 90% of NIC capacity",[682,8820,8821],{},"Bandwidth saturation",[667,8823,8824,8829,8832],{},[682,8825,8826],{},[28,8827,8828],{},"coturn_errors_total",[682,8830,8831],{},"Spike",[682,8833,8834],{},"Auth failures \u002F attacks",[48,8836,8838],{"className":1237,"code":8837,"language":1239,"meta":53,"style":53},"# prometheus\u002Frules\u002Fcoturn.yml\ngroups:\n  - name: coturn\n    rules:\n      - alert: TurnCapacityHigh\n        expr: coturn_current_allocations \u002F 1000 > 0.8\n        for: 2m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"TURN server at {{ $value | humanizePercentage }} capacity\"\n\n      - alert: TurnBandwidthSaturated\n        expr: rate(coturn_total_traffic_bytes[1m]) > 900000000\n        for: 1m\n        labels:\n          severity: critical\n",[28,8839,8840,8845,8851,8862,8868,8879,8888,8896,8902,8910,8916,8925,8929,8940,8949,8958,8964],{"__ignoreMap":53},[57,8841,8842],{"class":59,"line":60},[57,8843,8844],{"class":63},"# prometheus\u002Frules\u002Fcoturn.yml\n",[57,8846,8847,8849],{"class":59,"line":67},[57,8848,1252],{"class":1251},[57,8850,1255],{"class":254},[57,8852,8853,8855,8857,8859],{"class":59,"line":81},[57,8854,1260],{"class":254},[57,8856,1263],{"class":1251},[57,8858,1266],{"class":254},[57,8860,8861],{"class":74},"coturn\n",[57,8863,8864,8866],{"class":59,"line":91},[57,8865,1274],{"class":1251},[57,8867,1255],{"class":254},[57,8869,8870,8872,8874,8876],{"class":59,"line":98},[57,8871,1281],{"class":254},[57,8873,1870],{"class":1251},[57,8875,1266],{"class":254},[57,8877,8878],{"class":74},"TurnCapacityHigh\n",[57,8880,8881,8883,8885],{"class":59,"line":123},[57,8882,1294],{"class":1251},[57,8884,1266],{"class":254},[57,8886,8887],{"class":74},"coturn_current_allocations \u002F 1000 > 0.8\n",[57,8889,8890,8892,8894],{"class":59,"line":132},[57,8891,1899],{"class":1251},[57,8893,1266],{"class":254},[57,8895,2054],{"class":74},[57,8897,8898,8900],{"class":59,"line":143},[57,8899,1909],{"class":1251},[57,8901,1255],{"class":254},[57,8903,8904,8906,8908],{"class":59,"line":148},[57,8905,1916],{"class":1251},[57,8907,1266],{"class":254},[57,8909,2006],{"class":74},[57,8911,8912,8914],{"class":59,"line":154},[57,8913,1936],{"class":1251},[57,8915,1255],{"class":254},[57,8917,8918,8920,8922],{"class":59,"line":175},[57,8919,1943],{"class":1251},[57,8921,1266],{"class":254},[57,8923,8924],{"class":74},"\"TURN server at {{ $value | humanizePercentage }} capacity\"\n",[57,8926,8927],{"class":59,"line":190},[57,8928,95],{"emptyLinePlaceholder":94},[57,8930,8931,8933,8935,8937],{"class":59,"line":195},[57,8932,1281],{"class":254},[57,8934,1870],{"class":1251},[57,8936,1266],{"class":254},[57,8938,8939],{"class":74},"TurnBandwidthSaturated\n",[57,8941,8942,8944,8946],{"class":59,"line":207},[57,8943,1294],{"class":1251},[57,8945,1266],{"class":254},[57,8947,8948],{"class":74},"rate(coturn_total_traffic_bytes[1m]) > 900000000\n",[57,8950,8951,8953,8955],{"class":59,"line":216},[57,8952,1899],{"class":1251},[57,8954,1266],{"class":254},[57,8956,8957],{"class":74},"1m\n",[57,8959,8960,8962],{"class":59,"line":222},[57,8961,1909],{"class":1251},[57,8963,1255],{"class":254},[57,8965,8966,8968,8970],{"class":59,"line":490},[57,8967,1916],{"class":1251},[57,8969,1266],{"class":254},[57,8971,1921],{"class":74},[20,8973,8975],{"id":8974},"multi-region-deployment","Multi-Region Deployment",[16,8977,8978],{},"A single TURN server creates a single point of failure and adds latency for distant users. Deploy TURN servers in each region where your users are concentrated, and use GeoDNS or anycast to route ICE candidates to the nearest node.",[16,8980,8981],{},"Application server pattern:",[48,8983,8985],{"className":2885,"code":8984,"language":2887,"meta":53,"style":53},"\u002F\u002F Return region-appropriate TURN servers based on user location\nfunction getIceServers(userRegion) {\n  const servers = {\n    'us-east': 'turn-us-east.example.com',\n    'eu-west': 'turn-eu-west.example.com',\n    'ap-southeast': 'turn-ap.example.com',\n  };\n  const primary = servers[userRegion] || servers['us-east'];\n  return [\n    { urls: `stun:${primary}:3478` },\n    {\n      urls: [`turn:${primary}:3478`, `turns:${primary}:5349`],\n      username: credentials.username,\n      credential: credentials.password,\n    },\n  ];\n}\n",[28,8986,8987,8992,8997,9002,9007,9012,9017,9022,9027,9032,9037,9042,9047,9052,9057,9062,9067],{"__ignoreMap":53},[57,8988,8989],{"class":59,"line":60},[57,8990,8991],{},"\u002F\u002F Return region-appropriate TURN servers based on user location\n",[57,8993,8994],{"class":59,"line":67},[57,8995,8996],{},"function getIceServers(userRegion) {\n",[57,8998,8999],{"class":59,"line":81},[57,9000,9001],{},"  const servers = {\n",[57,9003,9004],{"class":59,"line":91},[57,9005,9006],{},"    'us-east': 'turn-us-east.example.com',\n",[57,9008,9009],{"class":59,"line":98},[57,9010,9011],{},"    'eu-west': 'turn-eu-west.example.com',\n",[57,9013,9014],{"class":59,"line":123},[57,9015,9016],{},"    'ap-southeast': 'turn-ap.example.com',\n",[57,9018,9019],{"class":59,"line":132},[57,9020,9021],{},"  };\n",[57,9023,9024],{"class":59,"line":143},[57,9025,9026],{},"  const primary = servers[userRegion] || servers['us-east'];\n",[57,9028,9029],{"class":59,"line":148},[57,9030,9031],{},"  return [\n",[57,9033,9034],{"class":59,"line":154},[57,9035,9036],{},"    { urls: `stun:${primary}:3478` },\n",[57,9038,9039],{"class":59,"line":175},[57,9040,9041],{},"    {\n",[57,9043,9044],{"class":59,"line":190},[57,9045,9046],{},"      urls: [`turn:${primary}:3478`, `turns:${primary}:5349`],\n",[57,9048,9049],{"class":59,"line":195},[57,9050,9051],{},"      username: credentials.username,\n",[57,9053,9054],{"class":59,"line":207},[57,9055,9056],{},"      credential: credentials.password,\n",[57,9058,9059],{"class":59,"line":216},[57,9060,9061],{},"    },\n",[57,9063,9064],{"class":59,"line":222},[57,9065,9066],{},"  ];\n",[57,9068,9069],{"class":59,"line":490},[57,9070,3810],{},[16,9072,9073],{},"Always include at least two TURN server URLs in the ICE configuration — UDP primary and TCP\u002FTLS fallback. Browsers automatically fall through to TCP and then TLS when UDP is blocked.",[20,9075,9077],{"id":9076},"operational-checks","Operational Checks",[16,9079,9080],{},"After deployment, validate with a TURN test client before sending production traffic:",[48,9082,9084],{"className":50,"code":9083,"language":52,"meta":53,"style":53},"# Install turnutils (comes with coturn)\nturnutils_uclient -t -u testuser -w secretpass -p 3478 turn.example.com\n\n# Check allocation success rate — should be 100%\n# Check round-trip time — should be \u003C 5ms on same-region VPS\n",[28,9085,9086,9091,9117,9121,9126],{"__ignoreMap":53},[57,9087,9088],{"class":59,"line":60},[57,9089,9090],{"class":63},"# Install turnutils (comes with coturn)\n",[57,9092,9093,9096,9099,9101,9103,9106,9109,9111,9114],{"class":59,"line":67},[57,9094,9095],{"class":101},"turnutils_uclient",[57,9097,9098],{"class":70}," -t",[57,9100,8514],{"class":70},[57,9102,8517],{"class":74},[57,9104,9105],{"class":70}," -w",[57,9107,9108],{"class":74}," secretpass",[57,9110,8525],{"class":70},[57,9112,9113],{"class":70}," 3478",[57,9115,9116],{"class":74}," turn.example.com\n",[57,9118,9119],{"class":59,"line":81},[57,9120,95],{"emptyLinePlaceholder":94},[57,9122,9123],{"class":59,"line":91},[57,9124,9125],{"class":63},"# Check allocation success rate — should be 100%\n",[57,9127,9128],{"class":59,"line":98},[57,9129,9130],{"class":63},"# Check round-trip time — should be \u003C 5ms on same-region VPS\n",[16,9132,9133,9134,9136,9137,9140],{},"A healthy TURN server shows allocation latency under 5ms and zero auth failures in the coturn log. If you see ",[28,9135,7343],{}," in logs, check that your application server's shared secret matches the ",[28,9138,9139],{},"static-auth-secret"," in turnserver.conf exactly — whitespace differences cause silent mismatches.",[1009,9142,9143],{},"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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":53,"searchDepth":67,"depth":67,"links":9145},[9146,9147,9148,9149,9150,9151,9152,9153,9154],{"id":8125,"depth":67,"text":8126},{"id":8155,"depth":67,"text":8156},{"id":8234,"depth":67,"text":8235},{"id":8280,"depth":67,"text":8281},{"id":8489,"depth":67,"text":8490},{"id":8633,"depth":67,"text":8634},{"id":8755,"depth":67,"text":8756},{"id":8974,"depth":67,"text":8975},{"id":9076,"depth":67,"text":9077},"2025-08-01","Step-by-step coturn configuration for production WebRTC: TLS setup, credential management, quota enforcement, Prometheus metrics, and multi-region deployment patterns.",{},"\u002Fblog\u002Fwebrtc-turn-server-deployment",{"title":8114,"description":9156},"blog\u002Fwebrtc-turn-server-deployment",[9162,9163,3504,9164,9165,1038],"coturn","turn-server","ice","stun","dlDvW-YjQBXQxhlzDlMzMCZAMHJUVFttTGWYmOgatm8",{"id":9168,"title":9169,"author":7,"body":9170,"category":4563,"coverImage":9904,"date":9905,"description":9906,"extension":1027,"meta":9907,"navigation":94,"path":9908,"readingTime":148,"seo":9909,"stem":9910,"tags":9911,"__hash__":9914},"posts\u002Fblog\u002Fasterisk-dialplan-best-practices.md","Asterisk Dialplan Best Practices for Production Systems",{"type":9,"value":9171,"toc":9892},[9172,9175,9178,9182,9189,9312,9316,9319,9358,9365,9369,9383,9426,9443,9447,9468,9530,9537,9541,9544,9559,9562,9649,9656,9660,9663,9719,9736,9740,9755,9758,9821,9824,9828,9878,9881,9890],[12,9173,9169],{"id":9174},"asterisk-dialplan-best-practices-for-production-systems",[16,9176,9177],{},"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.",[20,9179,9181],{"id":9180},"context-and-pattern-organization","Context and Pattern Organization",[16,9183,9184,9185,9188],{},"Every context is a namespace. Keep them small and purposeful. A common mistake is one monolithic ",[28,9186,9187],{},"[from-internal]"," context that grows to 500 lines. Split by function:",[48,9190,9192],{"className":406,"code":9191,"language":408,"meta":53,"style":53},"[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",[28,9193,9194,9199,9204,9209,9213,9218,9223,9228,9233,9238,9243,9247,9251,9255,9260,9264,9269,9274,9278,9283,9288,9293,9298,9303,9308],{"__ignoreMap":53},[57,9195,9196],{"class":59,"line":60},[57,9197,9198],{},"[globals]\n",[57,9200,9201],{"class":59,"line":67},[57,9202,9203],{},"TRUNK_OUTBOUND=PJSIP\u002Fcarrier-trunk\n",[57,9205,9206],{"class":59,"line":81},[57,9207,9208],{},"LOG_LEVEL=3\n",[57,9210,9211],{"class":59,"line":91},[57,9212,95],{"emptyLinePlaceholder":94},[57,9214,9215],{"class":59,"line":98},[57,9216,9217],{},"[from-pstn]\n",[57,9219,9220],{"class":59,"line":123},[57,9221,9222],{},"; Inbound from carrier — validate and route only\n",[57,9224,9225],{"class":59,"line":132},[57,9226,9227],{},"exten => _+1NXXXXXXXXX,1,NoOp(Inbound DID: ${EXTEN})\n",[57,9229,9230],{"class":59,"line":143},[57,9231,9232],{}," same => n,Set(DID=${EXTEN})\n",[57,9234,9235],{"class":59,"line":148},[57,9236,9237],{}," same => n,GoSub(sub-did-lookup,s,1(${DID}))\n",[57,9239,9240],{"class":59,"line":154},[57,9241,9242],{}," same => n,Goto(ivr-main,s,1)\n",[57,9244,9245],{"class":59,"line":175},[57,9246,7844],{},[57,9248,9249],{"class":59,"line":190},[57,9250,95],{"emptyLinePlaceholder":94},[57,9252,9253],{"class":59,"line":195},[57,9254,7776],{},[57,9256,9257],{"class":59,"line":207},[57,9258,9259],{},"; Authenticated SIP endpoints only\n",[57,9261,9262],{"class":59,"line":216},[57,9263,7810],{},[57,9265,9266],{"class":59,"line":222},[57,9267,9268],{},"exten => _011.,1,GoSub(sub-international,s,1(${EXTEN}))\n",[57,9270,9271],{"class":59,"line":490},[57,9272,9273],{},"exten => *98,1,VoiceMailMain(${CALLERID(num)}@default)\n",[57,9275,9276],{"class":59,"line":496},[57,9277,95],{"emptyLinePlaceholder":94},[57,9279,9280],{"class":59,"line":502},[57,9281,9282],{},"[sub-dialout]\n",[57,9284,9285],{"class":59,"line":507},[57,9286,9287],{},"; Reusable outbound dialing subroutine\n",[57,9289,9290],{"class":59,"line":513},[57,9291,9292],{},"exten => s,1,NoOp(Dialing: ${ARG1})\n",[57,9294,9295],{"class":59,"line":519},[57,9296,9297],{}," same => n,Set(CALLERID(num)=${OUTBOUND_CID})\n",[57,9299,9300],{"class":59,"line":525},[57,9301,9302],{}," same => n,Dial(${TRUNK_OUTBOUND}\u002F${ARG1},30,gTt)\n",[57,9304,9305],{"class":59,"line":531},[57,9306,9307],{}," same => n,GoSub(sub-handle-dialstatus,s,1)\n",[57,9309,9310],{"class":59,"line":536},[57,9311,7874],{},[1385,9313,9315],{"id":9314},"pattern-matching-priority","Pattern Matching Priority",[16,9317,9318],{},"Asterisk matches extensions by specificity, not order. Longer patterns beat shorter ones. Exact matches beat patterns. This trips teams up with overlapping international prefixes:",[48,9320,9322],{"className":406,"code":9321,"language":408,"meta":53,"style":53},"; 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",[28,9323,9324,9329,9334,9339,9343,9348,9353],{"__ignoreMap":53},[57,9325,9326],{"class":59,"line":60},[57,9327,9328],{},"; These DO NOT conflict — exact beats pattern\n",[57,9330,9331],{"class":59,"line":67},[57,9332,9333],{},"exten => 011972,1,NoOp(Israel direct)\n",[57,9335,9336],{"class":59,"line":81},[57,9337,9338],{},"exten => _011.,1,NoOp(Generic international)\n",[57,9340,9341],{"class":59,"line":91},[57,9342,95],{"emptyLinePlaceholder":94},[57,9344,9345],{"class":59,"line":98},[57,9346,9347],{},"; These DO conflict — same pattern length, Asterisk picks first loaded\n",[57,9349,9350],{"class":59,"line":123},[57,9351,9352],{},"exten => _NXXXXXXXXX,1,NoOp(10-digit US)\n",[57,9354,9355],{"class":59,"line":132},[57,9356,9357],{},"exten => _1XXXXXXXXX,1,NoOp(1+ US)   ; never reached for 1XXXXXXXXX\n",[16,9359,9360,9361,9364],{},"Run ",[28,9362,9363],{},"dialplan show \u003Ccontext>"," after every non-trivial change to see the compiled pattern tree.",[20,9366,9368],{"id":9367},"gosub-over-macro","GoSub Over Macro",[16,9370,9371,9372,9375,9376,9379,9380,9382],{},"Asterisk deprecated the ",[28,9373,9374],{},"Macro"," application in 16.x and removed it in 21.x. Use ",[28,9377,9378],{},"GoSub"," for all reusable logic. The key difference: ",[28,9381,9378],{}," 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.",[48,9384,9386],{"className":406,"code":9385,"language":408,"meta":53,"style":53},"; 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",[28,9387,9388,9393,9398,9402,9407,9412,9417,9422],{"__ignoreMap":53},[57,9389,9390],{"class":59,"line":60},[57,9391,9392],{},"; Correct: GoSub with arguments\n",[57,9394,9395],{"class":59,"line":67},[57,9396,9397],{},"exten => s,1,GoSub(sub-playback,s,1(ivr-welcome,en))\n",[57,9399,9400],{"class":59,"line":81},[57,9401,95],{"emptyLinePlaceholder":94},[57,9403,9404],{"class":59,"line":91},[57,9405,9406],{},"[sub-playback]\n",[57,9408,9409],{"class":59,"line":98},[57,9410,9411],{},"exten => s,1,NoOp(Playing ${ARG1} in ${ARG2})\n",[57,9413,9414],{"class":59,"line":123},[57,9415,9416],{}," same => n,Background(${ARG1})\n",[57,9418,9419],{"class":59,"line":132},[57,9420,9421],{}," same => n,WaitExten(5)\n",[57,9423,9424],{"class":59,"line":143},[57,9425,7874],{},[16,9427,9428,9429,2438,9432,9435,9436,9439,9440,4639],{},"Arguments are ",[28,9430,9431],{},"${ARG1}",[28,9433,9434],{},"${ARG2}",", etc. They do not leak into the calling context. Local variables set inside a subroutine with ",[28,9437,9438],{},"Set(LOCAL(var)=value)"," are stack-scoped and cleaned up on ",[28,9441,9442],{},"Return()",[20,9444,9446],{"id":9445},"handling-hangup-correctly","Handling Hangup Correctly",[16,9448,9449,9450,9453,9454,2438,9457,2438,9460,9463,9464,9467],{},"Unhandled hangups are the most common production dialplan bug. When a caller hangs up mid-call, Asterisk throws a ",[28,9451,9452],{},"HANGUP"," signal. If your dialplan has blocking applications (",[28,9455,9456],{},"WaitExten",[28,9458,9459],{},"Record",[28,9461,9462],{},"AGI","), they exit immediately and Asterisk executes the ",[28,9465,9466],{},"h"," extension in the current context.",[48,9469,9471],{"className":406,"code":9470,"language":408,"meta":53,"style":53},"[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",[28,9472,9473,9478,9483,9488,9493,9498,9502,9507,9511,9515,9520,9525],{"__ignoreMap":53},[57,9474,9475],{"class":59,"line":60},[57,9476,9477],{},"[ivr-main]\n",[57,9479,9480],{"class":59,"line":67},[57,9481,9482],{},"exten => s,1,Answer()\n",[57,9484,9485],{"class":59,"line":81},[57,9486,9487],{}," same => n,Background(welcome-message)\n",[57,9489,9490],{"class":59,"line":91},[57,9491,9492],{}," same => n,WaitExten(10)\n",[57,9494,9495],{"class":59,"line":98},[57,9496,9497],{}," same => n,Goto(ivr-main,timeout,1)\n",[57,9499,9500],{"class":59,"line":123},[57,9501,95],{"emptyLinePlaceholder":94},[57,9503,9504],{"class":59,"line":132},[57,9505,9506],{},"exten => timeout,1,Playback(please-try-again)\n",[57,9508,9509],{"class":59,"line":143},[57,9510,9242],{},[57,9512,9513],{"class":59,"line":148},[57,9514,95],{"emptyLinePlaceholder":94},[57,9516,9517],{"class":59,"line":154},[57,9518,9519],{},"; Always define h — even if just to log\n",[57,9521,9522],{"class":59,"line":175},[57,9523,9524],{},"exten => h,1,NoOp(Hangup in ivr-main for ${CALLERID(num)})\n",[57,9526,9527],{"class":59,"line":190},[57,9528,9529],{}," same => n,GoSub(sub-cdr-close,s,1)\n",[16,9531,9532,9533,9536],{},"Without ",[28,9534,9535],{},"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.",[20,9538,9540],{"id":9539},"agi-integration-patterns","AGI Integration Patterns",[16,9542,9543],{},"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.",[48,9545,9547],{"className":406,"code":9546,"language":408,"meta":53,"style":53},"exten => s,1,AGI(agi:\u002F\u002F127.0.0.1:4573\u002Froute-call)\n same => n,Goto(${AGIRESULT},1)\n",[28,9548,9549,9554],{"__ignoreMap":53},[57,9550,9551],{"class":59,"line":60},[57,9552,9553],{},"exten => s,1,AGI(agi:\u002F\u002F127.0.0.1:4573\u002Froute-call)\n",[57,9555,9556],{"class":59,"line":67},[57,9557,9558],{}," same => n,Goto(${AGIRESULT},1)\n",[16,9560,9561],{},"Your FastAGI server listens on port 4573, receives the AGI environment variables, and responds with AGI commands:",[48,9563,9565],{"className":1536,"code":9564,"language":1538,"meta":53,"style":53},"# 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",[28,9566,9567,9572,9577,9581,9586,9591,9596,9600,9605,9610,9615,9620,9625,9629,9634,9639,9644],{"__ignoreMap":53},[57,9568,9569],{"class":59,"line":60},[57,9570,9571],{},"# Minimal FastAGI handler (Python)\n",[57,9573,9574],{"class":59,"line":67},[57,9575,9576],{},"import socket, sys\n",[57,9578,9579],{"class":59,"line":81},[57,9580,95],{"emptyLinePlaceholder":94},[57,9582,9583],{"class":59,"line":91},[57,9584,9585],{},"def handle(conn):\n",[57,9587,9588],{"class":59,"line":98},[57,9589,9590],{},"    f = conn.makefile()\n",[57,9592,9593],{"class":59,"line":123},[57,9594,9595],{},"    env = {}\n",[57,9597,9598],{"class":59,"line":132},[57,9599,1696],{},[57,9601,9602],{"class":59,"line":143},[57,9603,9604],{},"        line = f.readline().strip()\n",[57,9606,9607],{"class":59,"line":148},[57,9608,9609],{},"        if not line:\n",[57,9611,9612],{"class":59,"line":154},[57,9613,9614],{},"            break\n",[57,9616,9617],{"class":59,"line":175},[57,9618,9619],{},"        key, _, val = line.partition(': ')\n",[57,9621,9622],{"class":59,"line":190},[57,9623,9624],{},"        env[key] = val\n",[57,9626,9627],{"class":59,"line":195},[57,9628,95],{"emptyLinePlaceholder":94},[57,9630,9631],{"class":59,"line":207},[57,9632,9633],{},"    # Send AGI command\n",[57,9635,9636],{"class":59,"line":216},[57,9637,9638],{},"    conn.sendall(b'SET VARIABLE AGIRESULT \"routed-context\"\\n')\n",[57,9640,9641],{"class":59,"line":222},[57,9642,9643],{},"    response = f.readline()  # 200 result=1\n",[57,9645,9646],{"class":59,"line":490},[57,9647,9648],{},"    conn.sendall(b'HANGUP\\n')\n",[16,9650,9651,9652,9655],{},"FastAGI responses must come within the ",[28,9653,9654],{},"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.",[20,9657,9659],{"id":9658},"variable-scoping-and-channel-variables","Variable Scoping and Channel Variables",[16,9661,9662],{},"Asterisk has three variable scopes:",[661,9664,9665,9678],{},[664,9666,9667],{},[667,9668,9669,9672,9675],{},[670,9670,9671],{},"Scope",[670,9673,9674],{},"Syntax",[670,9676,9677],{},"Lifetime",[677,9679,9680,9693,9706],{},[667,9681,9682,9685,9690],{},[682,9683,9684],{},"Channel",[682,9686,9687],{},[28,9688,9689],{},"${VAR}",[682,9691,9692],{},"Single channel, dies on hangup",[667,9694,9695,9698,9703],{},[682,9696,9697],{},"Global",[682,9699,9700],{},[28,9701,9702],{},"${GLOBAL(VAR)}",[682,9704,9705],{},"Process lifetime, shared across calls",[667,9707,9708,9711,9716],{},[682,9709,9710],{},"Subroutine local",[682,9712,9713],{},[28,9714,9715],{},"${LOCAL(VAR)}",[682,9717,9718],{},"Stack frame only",[16,9720,9721,9722,9725,9726,9729,9730,9733,9734,4639],{},"A common bug: setting a variable in a subroutine with ",[28,9723,9724],{},"Set(VAR=value)"," instead of ",[28,9727,9728],{},"Set(LOCAL(VAR)=value)",". The former leaks into the calling context and overwrites values you didn't intend to change. Use ",[28,9731,9732],{},"LOCAL()"," for any variable that should not persist after ",[28,9735,9442],{},[20,9737,9739],{"id":9738},"logging-and-debugging-in-production","Logging and Debugging in Production",[16,9741,9742,9743,9746,9747,9750,9751,9754],{},"Never rely on ",[28,9744,9745],{},"verbose"," logging in production — it floods the log file and impacts performance at high concurrency. Use ",[28,9748,9749],{},"NoOp"," sparingly and structured logging via the ",[28,9752,9753],{},"CEL"," (Channel Event Logging) subsystem instead.",[16,9756,9757],{},"Configure CEL to write to a PostgreSQL or MySQL backend:",[48,9759,9761],{"className":406,"code":9760,"language":408,"meta":53,"style":53},"; \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",[28,9762,9763,9768,9772,9777,9782,9787,9791,9796,9801,9806,9811,9816],{"__ignoreMap":53},[57,9764,9765],{"class":59,"line":60},[57,9766,9767],{},"; \u002Fetc\u002Fasterisk\u002Fcel.conf\n",[57,9769,9770],{"class":59,"line":67},[57,9771,5210],{},[57,9773,9774],{"class":59,"line":81},[57,9775,9776],{},"enable=yes\n",[57,9778,9779],{"class":59,"line":91},[57,9780,9781],{},"apps=all\n",[57,9783,9784],{"class":59,"line":98},[57,9785,9786],{},"events=CHANNEL_START,CHANNEL_END,ANSWER,HANGUP,BRIDGE_ENTER,BRIDGE_EXIT\n",[57,9788,9789],{"class":59,"line":123},[57,9790,95],{"emptyLinePlaceholder":94},[57,9792,9793],{"class":59,"line":132},[57,9794,9795],{},"; \u002Fetc\u002Fasterisk\u002Fcel_pgsql.conf\n",[57,9797,9798],{"class":59,"line":143},[57,9799,9800],{},"[global]\n",[57,9802,9803],{"class":59,"line":148},[57,9804,9805],{},"hostname=localhost\n",[57,9807,9808],{"class":59,"line":154},[57,9809,9810],{},"dbname=asterisk_cdr\n",[57,9812,9813],{"class":59,"line":175},[57,9814,9815],{},"password=secret\n",[57,9817,9818],{"class":59,"line":190},[57,9819,9820],{},"table=cel\n",[16,9822,9823],{},"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.",[20,9825,9827],{"id":9826},"performance-checklist-for-production","Performance Checklist for Production",[2315,9829,9830,9840,9854,9865,9871],{},[788,9831,4745,9832,9835,9836,9839],{},[28,9833,9834],{},"maxcalls"," in ",[28,9837,9838],{},"asterisk.conf"," to 110% of your expected peak. Without it, Asterisk accepts calls until memory exhausts.",[788,9841,9842,9843,9835,9846,9849,9850,9853],{},"Enable ",[28,9844,9845],{},"jbenable=yes",[28,9847,9848],{},"rtp.conf"," with ",[28,9851,9852],{},"jbmaxsize=200"," for calls crossing WAN links with variable latency.",[788,9855,9856,9857,9860,9861,9864],{},"Use ",[28,9858,9859],{},"PJSIP"," (chan_pjsip) instead of ",[28,9862,9863],{},"chan_sip"," — chan_sip is deprecated since Asterisk 17 and removed in 21.",[788,9866,4745,9867,9870],{},[28,9868,9869],{},"timers=yes"," in pjsip endpoints to send SIP OPTIONS keepalives and detect dead trunks within 30 seconds.",[788,9872,9873,9874,9877],{},"Pre-compile patterns with ",[28,9875,9876],{},"core set debug 0"," in production — debug mode disables some compiler optimizations in the pattern matcher.",[20,9879,9880],{"id":4910},"Putting It Together",[16,9882,9883,9884,9886,9887,9889],{},"A production Asterisk dialplan is not a script — it's a state machine where every transition must be explicit. Define the ",[28,9885,9466],{}," extension everywhere. Use ",[28,9888,9378],{}," 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.",[1009,9891,3478],{},{"title":53,"searchDepth":67,"depth":67,"links":9893},[9894,9897,9898,9899,9900,9901,9902,9903],{"id":9180,"depth":67,"text":9181,"children":9895},[9896],{"id":9314,"depth":81,"text":9315},{"id":9367,"depth":67,"text":9368},{"id":9445,"depth":67,"text":9446},{"id":9539,"depth":67,"text":9540},{"id":9658,"depth":67,"text":9659},{"id":9738,"depth":67,"text":9739},{"id":9826,"depth":67,"text":9827},{"id":4910,"depth":67,"text":9880},"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.",{},"\u002Fblog\u002Fasterisk-dialplan-best-practices",{"title":9169,"description":9906},"blog\u002Fasterisk-dialplan-best-practices",[2707,9912,5817,9913,8106],"dialplan","pbx","gPGNrB7reSO_I4DA26_IdENLXmCdISfNHBFBzaqcp98",{"id":9916,"title":9917,"author":7,"body":9918,"category":4563,"coverImage":1024,"date":10388,"description":10389,"extension":1027,"meta":10390,"navigation":94,"path":10391,"readingTime":154,"seo":10392,"stem":10393,"tags":10394,"__hash__":10397},"posts\u002Fblog\u002Fopensips-vs-kamailio-comparison.md","OpenSIPS vs Kamailio: Which SIP Proxy Should You Deploy?",{"type":9,"value":9919,"toc":10375},[9920,9923,9926,9930,9933,9945,9948,9952,9955,9959,9962,9968,9971,9975,9991,9997,10000,10004,10183,10188,10194,10198,10215,10229,10239,10243,10246,10317,10320,10324,10345,10349,10365,10369,10372],[12,9921,9917],{"id":9922},"opensips-vs-kamailio-which-sip-proxy-should-you-deploy",[16,9924,9925],{},"Choosing between OpenSIPS and Kamailio is one of the first decisions you make when building SIP infrastructure from scratch — and it shapes everything downstream. Both are open-source SIP proxies descended from the same SER (SIP Express Router) codebase. Both handle millions of calls per day in production. But they have diverged meaningfully in architecture, scripting model, and operational tooling. This post gives you the technical differentiators to make the call without guessing.",[20,9927,9929],{"id":9928},"origins-and-architecture","Origins and Architecture",[16,9931,9932],{},"OpenSIPS and Kamailio forked from SER around 2008. Since then they've accumulated different design philosophies:",[2315,9934,9935,9940],{},[788,9936,9937,9939],{},[2311,9938,7394],{}," stays closer to the SER roots: a monolithic process with a rich C module API and a mature pseudo-scripting language (kamailio.cfg). It prioritizes raw routing performance and has a well-understood memory model.",[788,9941,9942,9944],{},[2311,9943,7414],{}," introduced a multi-process worker architecture early on and has invested heavily in its scripting language (OpenSIPS Script), a management interface (MI), and higher-level abstractions like B2BUA and dialog management.",[16,9946,9947],{},"Both compile to native binaries. Neither runs in a VM or interpreter at call time. In raw SIP routing benchmarks on identical hardware, the performance difference is under 5% — irrelevant at any realistic scale below 10,000 calls per second.",[20,9949,9951],{"id":9950},"scripting-model","Scripting Model",[16,9953,9954],{},"This is where the two platforms diverge most visibly.",[1385,9956,9958],{"id":9957},"kamailio-configuration-kamailiocfg","Kamailio Configuration (kamailio.cfg)",[16,9960,9961],{},"Kamailio uses a C-like block structure with global parameters, module loading, and routing blocks. Routing logic lives in named route blocks called sequentially.",[48,9963,9966],{"className":9964,"code":9965,"language":654},[652],"request_route {\n    if (is_method(\"REGISTER\")) {\n        route(REGISTRAR);\n        exit;\n    }\n    if (!mf_process_maxfwd_header(10)) {\n        sl_send_reply(\"483\", \"Too Many Hops\");\n        exit;\n    }\n    route(DISPATCH);\n}\n\nroute[DISPATCH] {\n    if (!ds_select_dst(1, 4)) {\n        send_reply(\"503\", \"No Destination\");\n        exit;\n    }\n    t_relay();\n}\n",[28,9967,9965],{"__ignoreMap":53},[16,9969,9970],{},"Kamailio's scripting feels low-level but is extremely predictable. Every variable, AVP, and pseudo-variable has documented lifetime semantics.",[1385,9972,9974],{"id":9973},"opensips-script","OpenSIPS Script",[16,9976,9977,9978,2438,9981,9983,9984,6240,9987,9990],{},"OpenSIPS Script is closer to a proper language: it supports ",[28,9979,9980],{},"switch",[28,9982,6754],{},", and local variable scoping. The ",[28,9985,9986],{},"xlog",[28,9988,9989],{},"pv"," modules give string interpolation that Kamailio handles through pseudo-variables.",[48,9992,9995],{"className":9993,"code":9994,"language":654},[652],"route {\n    if (is_method(\"REGISTER\")) {\n        do_registration();\n        exit;\n    }\n\n    $var(tries) = 0;\n    while ($var(tries) \u003C 3) {\n        if (ds_select_dst(1, 4)) {\n            t_relay();\n            exit;\n        }\n        $var(tries) = $var(tries) + 1;\n    }\n    send_reply(503, \"No route available\");\n}\n",[28,9996,9994],{"__ignoreMap":53},[16,9998,9999],{},"For complex routing logic with loops and conditionals, OpenSIPS Script is more readable. For teams already fluent in C or shell scripting, Kamailio's model is immediately familiar.",[20,10001,10003],{"id":10002},"module-ecosystem-comparison","Module Ecosystem Comparison",[661,10005,10006,10017],{},[664,10007,10008],{},[667,10009,10010,10013,10015],{},[670,10011,10012],{},"Feature",[670,10014,7394],{},[670,10016,7414],{},[677,10018,10019,10033,10046,10061,10075,10096,10110,10128,10142,10156,10170],{},[667,10020,10021,10024,10029],{},[682,10022,10023],{},"Registrar",[682,10025,10026],{},[28,10027,10028],{},"registrar",[682,10030,10031],{},[28,10032,10028],{},[667,10034,10035,10038,10042],{},[682,10036,10037],{},"Dispatcher (load balance)",[682,10039,10040],{},[28,10041,4960],{},[682,10043,10044],{},[28,10045,4960],{},[667,10047,10048,10051,10056],{},[682,10049,10050],{},"B2BUA",[682,10052,10053],{},[28,10054,10055],{},"b2b_entities",[682,10057,10058,10060],{},[28,10059,10055],{}," (more mature)",[667,10062,10063,10066,10071],{},[682,10064,10065],{},"Dialog tracking",[682,10067,10068],{},[28,10069,10070],{},"dialog",[682,10072,10073],{},[28,10074,10070],{},[667,10076,10077,10080,10088],{},[682,10078,10079],{},"REST API \u002F MI",[682,10081,10082,2438,10085],{},[28,10083,10084],{},"xhttp",[28,10086,10087],{},"jsonrpc-s",[682,10089,10090,2438,10093],{},[28,10091,10092],{},"mi_http",[28,10094,10095],{},"mi_fifo",[667,10097,10098,10101,10106],{},[682,10099,10100],{},"Presence \u002F PUBLISH",[682,10102,10103],{},[28,10104,10105],{},"presence",[682,10107,10108],{},[28,10109,10105],{},[667,10111,10112,10115,10120],{},[682,10113,10114],{},"WebSocket \u002F WebRTC",[682,10116,10117],{},[28,10118,10119],{},"websocket",[682,10121,10122,2438,10125],{},[28,10123,10124],{},"proto_ws",[28,10126,10127],{},"proto_wss",[667,10129,10130,10133,10136],{},[682,10131,10132],{},"Call center queuing",[682,10134,10135],{},"Limited",[682,10137,10138,10141],{},[28,10139,10140],{},"callcenter"," module",[667,10143,10144,10147,10152],{},[682,10145,10146],{},"Homer SIPcapture",[682,10148,10149],{},[28,10150,10151],{},"sipcapture",[682,10153,10154],{},[28,10155,10151],{},[667,10157,10158,10161,10166],{},[682,10159,10160],{},"Lua scripting",[682,10162,10163],{},[28,10164,10165],{},"app_lua",[682,10167,10168,10141],{},[28,10169,4175],{},[667,10171,10172,10175,10180],{},[682,10173,10174],{},"Python scripting",[682,10176,10177],{},[28,10178,10179],{},"app_python3",[682,10181,10182],{},"None (use Lua or Go)",[16,10184,7403,10185,10187],{},[28,10186,10179],{}," module is a significant differentiator if your team writes Python. It lets you call arbitrary Python code inside routing blocks, which is useful for integrating with internal APIs, databases, or ML-based fraud detection without writing C modules.",[16,10189,10190,10191,10193],{},"OpenSIPS's ",[28,10192,10140],{}," module provides ACD (automatic call distribution) queuing logic in pure OpenSIPS Script — Kamailio has no equivalent and you'd build it in Lua or an external application server.",[20,10195,10197],{"id":10196},"management-and-operations","Management and Operations",[16,10199,10200,10201,10204,10205,10207,10208,6240,10211,10214],{},"Kamailio exposes control via ",[28,10202,10203],{},"kamctl"," (CLI) and ",[28,10206,4658],{}," (socket-based FIFO). The ",[28,10209,10210],{},"XMLRPC",[28,10212,10213],{},"JSONRPC"," modules enable HTTP-based management. Kamailio's state is not centralized — each process reads shared memory, which means cluster-wide state changes require sending commands to each node.",[16,10216,10217,10218,10220,10221,10224,10225,10228],{},"OpenSIPS ships with a built-in HTTP management interface (",[28,10219,10092],{},") and the standalone ",[28,10222,10223],{},"opensips-cli"," tool that talks to the MI layer. The ",[28,10226,10227],{},"clusterer"," module provides native cluster state replication: push a dispatcher list update to one node and it propagates automatically. For large clusters, this difference is operationally significant.",[16,10230,10231,10232,6240,10235,10238],{},"Both support hot-reloading of routing configuration via ",[28,10233,10234],{},"kamcmd cfg.reload",[28,10236,10237],{},"opensips-cli mi reload_routes"," respectively — no restart required for routing changes.",[20,10240,10242],{"id":10241},"performance-at-scale","Performance at Scale",[16,10244,10245],{},"Synthetic benchmarks (SIPp, 10,000 INVITE\u002Fsec, no media, 4-core VM):",[661,10247,10248,10260],{},[664,10249,10250],{},[667,10251,10252,10254,10257],{},[670,10253,1750],{},[670,10255,10256],{},"Kamailio 5.8",[670,10258,10259],{},"OpenSIPS 3.4",[677,10261,10262,10273,10284,10295,10306],{},[667,10263,10264,10267,10270],{},[682,10265,10266],{},"Max INVITE\u002Fsec (stateless)",[682,10268,10269],{},"~48,000",[682,10271,10272],{},"~45,000",[667,10274,10275,10278,10281],{},[682,10276,10277],{},"Max INVITE\u002Fsec (stateful)",[682,10279,10280],{},"~22,000",[682,10282,10283],{},"~20,000",[667,10285,10286,10289,10292],{},[682,10287,10288],{},"Memory per 10k dialogs",[682,10290,10291],{},"~180 MB",[682,10293,10294],{},"~210 MB",[667,10296,10297,10300,10303],{},[682,10298,10299],{},"Worker process count",[682,10301,10302],{},"Single process + workers",[682,10304,10305],{},"Multi-process",[667,10307,10308,10311,10314],{},[682,10309,10310],{},"CPU at 5k calls\u002Fsec",[682,10312,10313],{},"35% (4 cores)",[682,10315,10316],{},"38% (4 cores)",[16,10318,10319],{},"These numbers compress at high concurrency because OpenSIPS's worker isolation reduces lock contention. Above 30,000 concurrent calls, OpenSIPS's multi-process model can outperform Kamailio's shared-memory approach on NUMA hardware.",[20,10321,10323],{"id":10322},"when-to-choose-kamailio","When to Choose Kamailio",[2315,10325,10326,10329,10332,10335],{},[788,10327,10328],{},"Your team writes Python or Lua and wants scripting inside routing logic.",[788,10330,10331],{},"You need a well-documented, battle-tested SIP registrar and proxy with minimal operational complexity.",[788,10333,10334],{},"You're running a stateless SIP proxy at very high throughput (carrier peering, SBC front-end).",[788,10336,10337,10338,6240,10341,10344],{},"You want deep Asterisk or FreeSWITCH integration via ",[28,10339,10340],{},"tm",[28,10342,10343],{},"uac"," modules.",[20,10346,10348],{"id":10347},"when-to-choose-opensips","When to Choose OpenSIPS",[2315,10350,10351,10354,10359,10362],{},[788,10352,10353],{},"You need built-in ACD call queuing without an external application server.",[788,10355,10356,10357,4639],{},"You're building a multi-node cluster and want native state replication via ",[28,10358,10227],{},[788,10360,10361],{},"Your routing logic has complex branching that benefits from proper loop and scoping constructs.",[788,10363,10364],{},"You want a first-class HTTP management API without bolting on additional modules.",[20,10366,10368],{"id":10367},"the-honest-answer","The Honest Answer",[16,10370,10371],{},"For pure SIP proxy and load balancing, either works. The decision usually comes down to your team's scripting comfort and which module covers your specific use case out of the box. Run both in a lab against your actual traffic pattern for 48 hours before committing. The benchmark that matters is your workload, not SIPp.",[16,10373,10374],{},"If you're building a carrier-grade platform and need to pick one: Kamailio for stateless high-throughput routing, OpenSIPS for complex stateful call flows with cluster replication.",{"title":53,"searchDepth":67,"depth":67,"links":10376},[10377,10378,10382,10383,10384,10385,10386,10387],{"id":9928,"depth":67,"text":9929},{"id":9950,"depth":67,"text":9951,"children":10379},[10380,10381],{"id":9957,"depth":81,"text":9958},{"id":9973,"depth":81,"text":9974},{"id":10002,"depth":67,"text":10003},{"id":10196,"depth":67,"text":10197},{"id":10241,"depth":67,"text":10242},{"id":10322,"depth":67,"text":10323},{"id":10347,"depth":67,"text":10348},{"id":10367,"depth":67,"text":10368},"2025-06-01","A technical comparison of OpenSIPS and Kamailio covering routing flexibility, module ecosystems, performance benchmarks, and operational tradeoffs for production SIP infrastructure.",{},"\u002Fblog\u002Fopensips-vs-kamailio-comparison",{"title":9917,"description":10389},"blog\u002Fopensips-vs-kamailio-comparison",[7413,1036,10395,10396,4962],"sip-proxy","comparison","69pVQLbIp619eTbtoVrELRZzUUGKzwwtD6203pmAPCs",1776974166862]