[{"data":1,"prerenderedAt":1176},["ShallowReactive",2],{"post-\u002Fblog\u002Fwebrtc-turn-server-deployment":3},{"id":4,"title":5,"author":6,"body":7,"category":1159,"coverImage":1160,"date":1161,"description":1162,"extension":1163,"meta":1164,"navigation":182,"path":1165,"readingTime":255,"seo":1166,"stem":1167,"tags":1168,"__hash__":1175},"posts\u002Fblog\u002Fwebrtc-turn-server-deployment.md","Deploying a Production TURN Server: coturn Configuration Guide","Tumarm Engineering",{"type":8,"value":9,"toc":1148},"minimark",[10,14,18,23,26,49,52,56,59,137,140,144,206,210,451,458,462,465,515,518,584,587,602,605,609,720,727,731,738,741,813,971,975,978,981,1073,1076,1080,1083,1133,1144],[11,12,5],"h1",{"id":13},"deploying-a-production-turn-server-coturn-configuration-guide",[15,16,17],"p",{},"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.",[19,20,22],"h2",{"id":21},"how-turn-fits-into-webrtc-ice","How TURN Fits Into WebRTC ICE",[15,24,25],{},"When two WebRTC peers connect, the ICE (Interactive Connectivity Establishment) process collects candidate addresses from three sources:",[27,28,29,37,43],"ol",{},[30,31,32,36],"li",{},[33,34,35],"strong",{},"Host candidates"," — the interface's own IP addresses",[30,38,39,42],{},[33,40,41],{},"Server-reflexive candidates"," — the public IP seen by a STUN server",[30,44,45,48],{},[33,46,47],{},"Relay candidates"," — IP:port pairs allocated on the TURN server",[15,50,51],{},"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.",[19,53,55],{"id":54},"hardware-sizing","Hardware Sizing",[15,57,58],{},"TURN relay is bandwidth-bound, not compute-bound. Each relayed call uses two UDP streams at the TURN server.",[60,61,62,81],"table",{},[63,64,65],"thead",{},[66,67,68,72,75,78],"tr",{},[69,70,71],"th",{},"Concurrent relayed calls",[69,73,74],{},"Bitrate per call",[69,76,77],{},"Required throughput",[69,79,80],{},"Recommended NIC",[82,83,84,99,111,124],"tbody",{},[66,85,86,90,93,96],{},[87,88,89],"td",{},"100",[87,91,92],{},"100 Kbps audio",[87,94,95],{},"20 Mbps",[87,97,98],{},"100 Mbps",[66,100,101,104,106,108],{},[87,102,103],{},"500",[87,105,92],{},[87,107,98],{},[87,109,110],{},"1 Gbps",[66,112,113,116,119,121],{},[87,114,115],{},"1,000",[87,117,118],{},"500 Kbps video",[87,120,110],{},[87,122,123],{},"10 Gbps",[66,125,126,129,131,134],{},[87,127,128],{},"5,000",[87,130,118],{},[87,132,133],{},"5 Gbps",[87,135,136],{},"10 Gbps bonded",[15,138,139],{},"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.",[19,141,143],{"id":142},"installing-coturn","Installing coturn",[145,146,151],"pre",{"className":147,"code":148,"language":149,"meta":150,"style":150},"language-bash shiki shiki-themes github-light github-dark","# 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","bash","",[152,153,154,163,177,184,190],"code",{"__ignoreMap":150},[155,156,159],"span",{"class":157,"line":158},"line",1,[155,160,162],{"class":161},"sJ8bj","# Debian \u002F Ubuntu\n",[155,164,166,170,174],{"class":157,"line":165},2,[155,167,169],{"class":168},"sScJk","apt-get",[155,171,173],{"class":172},"sZZnC"," install",[155,175,176],{"class":172}," coturn\n",[155,178,180],{"class":157,"line":179},3,[155,181,183],{"emptyLinePlaceholder":182},true,"\n",[155,185,187],{"class":157,"line":186},4,[155,188,189],{"class":161},"# Enable the service\n",[155,191,193,196,200,203],{"class":157,"line":192},5,[155,194,195],{"class":168},"sed",[155,197,199],{"class":198},"sj4cs"," -i",[155,201,202],{"class":172}," 's\u002F#TURNSERVER_ENABLED=1\u002FTURNSERVER_ENABLED=1\u002F'",[155,204,205],{"class":172}," \u002Fetc\u002Fdefault\u002Fcoturn\n",[19,207,209],{"id":208},"core-configuration-etcturnserverconf","Core Configuration (\u002Fetc\u002Fturnserver.conf)",[145,211,215],{"className":212,"code":213,"language":214,"meta":150,"style":150},"language-ini shiki shiki-themes github-light github-dark","# 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","ini",[152,216,217,222,227,232,237,242,248,253,259,265,271,276,282,288,294,300,306,312,318,323,329,335,340,346,352,358,363,369,375,381,387,393,399,404,410,416,422,428,433,439,445],{"__ignoreMap":150},[155,218,219],{"class":157,"line":158},[155,220,221],{},"# Network\n",[155,223,224],{"class":157,"line":165},[155,225,226],{},"listening-port=3478\n",[155,228,229],{"class":157,"line":179},[155,230,231],{},"tls-listening-port=5349\n",[155,233,234],{"class":157,"line":186},[155,235,236],{},"listening-ip=0.0.0.0\n",[155,238,239],{"class":157,"line":192},[155,240,241],{},"relay-ip=YOUR_PUBLIC_IP\n",[155,243,245],{"class":157,"line":244},6,[155,246,247],{},"external-ip=YOUR_PUBLIC_IP\n",[155,249,251],{"class":157,"line":250},7,[155,252,183],{"emptyLinePlaceholder":182},[155,254,256],{"class":157,"line":255},8,[155,257,258],{},"# Authentication — use long-term credential mechanism\n",[155,260,262],{"class":157,"line":261},9,[155,263,264],{},"lt-cred-mech\n",[155,266,268],{"class":157,"line":267},10,[155,269,270],{},"realm=turn.example.com\n",[155,272,274],{"class":157,"line":273},11,[155,275,183],{"emptyLinePlaceholder":182},[155,277,279],{"class":157,"line":278},12,[155,280,281],{},"# TLS — use a real cert, not self-signed\n",[155,283,285],{"class":157,"line":284},13,[155,286,287],{},"cert=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fturn.example.com\u002Ffullchain.pem\n",[155,289,291],{"class":157,"line":290},14,[155,292,293],{},"pkey=\u002Fetc\u002Fletsencrypt\u002Flive\u002Fturn.example.com\u002Fprivkey.pem\n",[155,295,297],{"class":157,"line":296},15,[155,298,299],{},"cipher-list=\"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256\"\n",[155,301,303],{"class":157,"line":302},16,[155,304,305],{},"no-sslv3\n",[155,307,309],{"class":157,"line":308},17,[155,310,311],{},"no-tlsv1\n",[155,313,315],{"class":157,"line":314},18,[155,316,317],{},"no-tlsv1_1\n",[155,319,321],{"class":157,"line":320},19,[155,322,183],{"emptyLinePlaceholder":182},[155,324,326],{"class":157,"line":325},20,[155,327,328],{},"# Database for credentials\n",[155,330,332],{"class":157,"line":331},21,[155,333,334],{},"userdb=\u002Fvar\u002Flib\u002Fturn\u002Fturndb\n",[155,336,338],{"class":157,"line":337},22,[155,339,183],{"emptyLinePlaceholder":182},[155,341,343],{"class":157,"line":342},23,[155,344,345],{},"# Logging\n",[155,347,349],{"class":157,"line":348},24,[155,350,351],{},"log-file=\u002Fvar\u002Flog\u002Fcoturn\u002Fturnserver.log\n",[155,353,355],{"class":157,"line":354},25,[155,356,357],{},"verbose\n",[155,359,361],{"class":157,"line":360},26,[155,362,183],{"emptyLinePlaceholder":182},[155,364,366],{"class":157,"line":365},27,[155,367,368],{},"# Security hardening\n",[155,370,372],{"class":157,"line":371},28,[155,373,374],{},"no-loopback-peers\n",[155,376,378],{"class":157,"line":377},29,[155,379,380],{},"no-multicast-peers\n",[155,382,384],{"class":157,"line":383},30,[155,385,386],{},"denied-peer-ip=10.0.0.0-10.255.255.255\n",[155,388,390],{"class":157,"line":389},31,[155,391,392],{},"denied-peer-ip=192.168.0.0-192.168.255.255\n",[155,394,396],{"class":157,"line":395},32,[155,397,398],{},"denied-peer-ip=172.16.0.0-172.31.255.255\n",[155,400,402],{"class":157,"line":401},33,[155,403,183],{"emptyLinePlaceholder":182},[155,405,407],{"class":157,"line":406},34,[155,408,409],{},"# Quota enforcement\n",[155,411,413],{"class":157,"line":412},35,[155,414,415],{},"user-quota=10\n",[155,417,419],{"class":157,"line":418},36,[155,420,421],{},"total-quota=1000\n",[155,423,425],{"class":157,"line":424},37,[155,426,427],{},"max-bps=500000\n",[155,429,431],{"class":157,"line":430},38,[155,432,183],{"emptyLinePlaceholder":182},[155,434,436],{"class":157,"line":435},39,[155,437,438],{},"# Prometheus metrics\n",[155,440,442],{"class":157,"line":441},40,[155,443,444],{},"prometheus\n",[155,446,448],{"class":157,"line":447},41,[155,449,450],{},"prometheus-port=9641\n",[15,452,453,454,457],{},"The ",[152,455,456],{},"denied-peer-ip"," 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.",[19,459,461],{"id":460},"credential-management","Credential Management",[15,463,464],{},"coturn uses SQLite by default. For multi-server deployments, switch to PostgreSQL or Redis so all nodes share the same credential store.",[145,466,468],{"className":147,"code":467,"language":149,"meta":150,"style":150},"# 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",[152,469,470,475,501,505,510],{"__ignoreMap":150},[155,471,472],{"class":157,"line":158},[155,473,474],{"class":161},"# Add a static user (for testing only)\n",[155,476,477,480,483,486,489,492,495,498],{"class":157,"line":165},[155,478,479],{"class":168},"turnadmin",[155,481,482],{"class":198}," -a",[155,484,485],{"class":198}," -u",[155,487,488],{"class":172}," testuser",[155,490,491],{"class":198}," -r",[155,493,494],{"class":172}," turn.example.com",[155,496,497],{"class":198}," -p",[155,499,500],{"class":172}," secretpass\n",[155,502,503],{"class":157,"line":179},[155,504,183],{"emptyLinePlaceholder":182},[155,506,507],{"class":157,"line":186},[155,508,509],{"class":161},"# Generate a time-limited credential (for production)\n",[155,511,512],{"class":157,"line":192},[155,513,514],{"class":161},"# HMAC-SHA1 of \"timestamp:username\" with your shared secret\n",[15,516,517],{},"For production WebRTC applications, use the REST API credential pattern. Your application server generates short-lived TURN credentials on demand:",[145,519,523],{"className":520,"code":521,"language":522,"meta":150,"style":150},"language-python shiki shiki-themes github-light github-dark","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","python",[152,524,525,530,534,539,544,549,554,559,564,569,574,579],{"__ignoreMap":150},[155,526,527],{"class":157,"line":158},[155,528,529],{},"import hmac, hashlib, base64, time\n",[155,531,532],{"class":157,"line":165},[155,533,183],{"emptyLinePlaceholder":182},[155,535,536],{"class":157,"line":179},[155,537,538],{},"def generate_turn_credentials(username, secret, ttl=3600):\n",[155,540,541],{"class":157,"line":186},[155,542,543],{},"    timestamp = int(time.time()) + ttl\n",[155,545,546],{"class":157,"line":192},[155,547,548],{},"    turn_user = f\"{timestamp}:{username}\"\n",[155,550,551],{"class":157,"line":244},[155,552,553],{},"    dig = hmac.new(\n",[155,555,556],{"class":157,"line":250},[155,557,558],{},"        secret.encode(),\n",[155,560,561],{"class":157,"line":255},[155,562,563],{},"        turn_user.encode(),\n",[155,565,566],{"class":157,"line":261},[155,567,568],{},"        hashlib.sha1\n",[155,570,571],{"class":157,"line":267},[155,572,573],{},"    ).digest()\n",[155,575,576],{"class":157,"line":273},[155,577,578],{},"    password = base64.b64encode(dig).decode()\n",[155,580,581],{"class":157,"line":278},[155,582,583],{},"    return {\"username\": turn_user, \"password\": password}\n",[15,585,586],{},"Configure coturn to validate these credentials:",[145,588,590],{"className":212,"code":589,"language":214,"meta":150,"style":150},"use-auth-secret\nstatic-auth-secret=YOUR_SHARED_SECRET\n",[152,591,592,597],{"__ignoreMap":150},[155,593,594],{"class":157,"line":158},[155,595,596],{},"use-auth-secret\n",[155,598,599],{"class":157,"line":165},[155,600,601],{},"static-auth-secret=YOUR_SHARED_SECRET\n",[15,603,604],{},"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.",[19,606,608],{"id":607},"firewall-rules","Firewall Rules",[145,610,612],{"className":147,"code":611,"language":149,"meta":150,"style":150},"# 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",[152,613,614,619,633,645,657,669,673,678,687,691,696],{"__ignoreMap":150},[155,615,616],{"class":157,"line":158},[155,617,618],{"class":161},"# Required ports\n",[155,620,621,624,627,630],{"class":157,"line":165},[155,622,623],{"class":168},"ufw",[155,625,626],{"class":172}," allow",[155,628,629],{"class":172}," 3478\u002Fudp",[155,631,632],{"class":161},"   # STUN\u002FTURN\n",[155,634,635,637,639,642],{"class":157,"line":179},[155,636,623],{"class":168},[155,638,626],{"class":172},[155,640,641],{"class":172}," 3478\u002Ftcp",[155,643,644],{"class":161},"   # TURN over TCP\n",[155,646,647,649,651,654],{"class":157,"line":186},[155,648,623],{"class":168},[155,650,626],{"class":172},[155,652,653],{"class":172}," 5349\u002Ftcp",[155,655,656],{"class":161},"   # TURNS (TLS)\n",[155,658,659,661,663,666],{"class":157,"line":192},[155,660,623],{"class":168},[155,662,626],{"class":172},[155,664,665],{"class":172}," 5349\u002Fudp",[155,667,668],{"class":161},"   # TURNS (DTLS)\n",[155,670,671],{"class":157,"line":244},[155,672,183],{"emptyLinePlaceholder":182},[155,674,675],{"class":157,"line":250},[155,676,677],{"class":161},"# Relay port range — must match coturn config\n",[155,679,680,682,684],{"class":157,"line":255},[155,681,623],{"class":168},[155,683,626],{"class":172},[155,685,686],{"class":172}," 49152:65535\u002Fudp\n",[155,688,689],{"class":157,"line":261},[155,690,183],{"emptyLinePlaceholder":182},[155,692,693],{"class":157,"line":267},[155,694,695],{"class":161},"# Prometheus scrape (from monitoring server only)\n",[155,697,698,700,702,705,708,711,714,717],{"class":157,"line":273},[155,699,623],{"class":168},[155,701,626],{"class":172},[155,703,704],{"class":172}," from",[155,706,707],{"class":172}," MONITOR_IP",[155,709,710],{"class":172}," to",[155,712,713],{"class":172}," any",[155,715,716],{"class":172}," port",[155,718,719],{"class":198}," 9641\n",[15,721,722,723,726],{},"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 ",[152,724,725],{},"total-quota * 2"," ports.",[19,728,730],{"id":729},"prometheus-metrics-and-alerting","Prometheus Metrics and Alerting",[15,732,733,734,737],{},"coturn exposes Prometheus metrics on port 9641 when ",[152,735,736],{},"prometheus"," is set in the config.",[15,739,740],{},"Key metrics to alert on:",[60,742,743,756],{},[63,744,745],{},[66,746,747,750,753],{},[69,748,749],{},"Metric",[69,751,752],{},"Alert threshold",[69,754,755],{},"Meaning",[82,757,758,771,787,800],{},[66,759,760,765,768],{},[87,761,762],{},[152,763,764],{},"coturn_total_allocations_quota_exceeded_total",[87,766,767],{},"> 0 per minute",[87,769,770],{},"Users hitting quota limits",[66,772,773,778,784],{},[87,774,775],{},[152,776,777],{},"coturn_current_allocations",[87,779,780,781],{},"> 80% of ",[152,782,783],{},"total-quota",[87,785,786],{},"Approaching capacity",[66,788,789,794,797],{},[87,790,791],{},[152,792,793],{},"coturn_total_traffic_bytes",[87,795,796],{},"Rate > 90% of NIC capacity",[87,798,799],{},"Bandwidth saturation",[66,801,802,807,810],{},[87,803,804],{},[152,805,806],{},"coturn_errors_total",[87,808,809],{},"Spike",[87,811,812],{},"Auth failures \u002F attacks",[145,814,818],{"className":815,"code":816,"language":817,"meta":150,"style":150},"language-yaml shiki shiki-themes github-light github-dark","# 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","yaml",[152,819,820,825,835,849,856,869,879,889,896,906,913,923,927,938,947,956,962],{"__ignoreMap":150},[155,821,822],{"class":157,"line":158},[155,823,824],{"class":161},"# prometheus\u002Frules\u002Fcoturn.yml\n",[155,826,827,831],{"class":157,"line":165},[155,828,830],{"class":829},"s9eBZ","groups",[155,832,834],{"class":833},"sVt8B",":\n",[155,836,837,840,843,846],{"class":157,"line":179},[155,838,839],{"class":833},"  - ",[155,841,842],{"class":829},"name",[155,844,845],{"class":833},": ",[155,847,848],{"class":172},"coturn\n",[155,850,851,854],{"class":157,"line":186},[155,852,853],{"class":829},"    rules",[155,855,834],{"class":833},[155,857,858,861,864,866],{"class":157,"line":192},[155,859,860],{"class":833},"      - ",[155,862,863],{"class":829},"alert",[155,865,845],{"class":833},[155,867,868],{"class":172},"TurnCapacityHigh\n",[155,870,871,874,876],{"class":157,"line":244},[155,872,873],{"class":829},"        expr",[155,875,845],{"class":833},[155,877,878],{"class":172},"coturn_current_allocations \u002F 1000 > 0.8\n",[155,880,881,884,886],{"class":157,"line":250},[155,882,883],{"class":829},"        for",[155,885,845],{"class":833},[155,887,888],{"class":172},"2m\n",[155,890,891,894],{"class":157,"line":255},[155,892,893],{"class":829},"        labels",[155,895,834],{"class":833},[155,897,898,901,903],{"class":157,"line":261},[155,899,900],{"class":829},"          severity",[155,902,845],{"class":833},[155,904,905],{"class":172},"warning\n",[155,907,908,911],{"class":157,"line":267},[155,909,910],{"class":829},"        annotations",[155,912,834],{"class":833},[155,914,915,918,920],{"class":157,"line":273},[155,916,917],{"class":829},"          summary",[155,919,845],{"class":833},[155,921,922],{"class":172},"\"TURN server at {{ $value | humanizePercentage }} capacity\"\n",[155,924,925],{"class":157,"line":278},[155,926,183],{"emptyLinePlaceholder":182},[155,928,929,931,933,935],{"class":157,"line":284},[155,930,860],{"class":833},[155,932,863],{"class":829},[155,934,845],{"class":833},[155,936,937],{"class":172},"TurnBandwidthSaturated\n",[155,939,940,942,944],{"class":157,"line":290},[155,941,873],{"class":829},[155,943,845],{"class":833},[155,945,946],{"class":172},"rate(coturn_total_traffic_bytes[1m]) > 900000000\n",[155,948,949,951,953],{"class":157,"line":296},[155,950,883],{"class":829},[155,952,845],{"class":833},[155,954,955],{"class":172},"1m\n",[155,957,958,960],{"class":157,"line":302},[155,959,893],{"class":829},[155,961,834],{"class":833},[155,963,964,966,968],{"class":157,"line":308},[155,965,900],{"class":829},[155,967,845],{"class":833},[155,969,970],{"class":172},"critical\n",[19,972,974],{"id":973},"multi-region-deployment","Multi-Region Deployment",[15,976,977],{},"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.",[15,979,980],{},"Application server pattern:",[145,982,986],{"className":983,"code":984,"language":985,"meta":150,"style":150},"language-javascript shiki shiki-themes github-light github-dark","\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","javascript",[152,987,988,993,998,1003,1008,1013,1018,1023,1028,1033,1038,1043,1048,1053,1058,1063,1068],{"__ignoreMap":150},[155,989,990],{"class":157,"line":158},[155,991,992],{},"\u002F\u002F Return region-appropriate TURN servers based on user location\n",[155,994,995],{"class":157,"line":165},[155,996,997],{},"function getIceServers(userRegion) {\n",[155,999,1000],{"class":157,"line":179},[155,1001,1002],{},"  const servers = {\n",[155,1004,1005],{"class":157,"line":186},[155,1006,1007],{},"    'us-east': 'turn-us-east.example.com',\n",[155,1009,1010],{"class":157,"line":192},[155,1011,1012],{},"    'eu-west': 'turn-eu-west.example.com',\n",[155,1014,1015],{"class":157,"line":244},[155,1016,1017],{},"    'ap-southeast': 'turn-ap.example.com',\n",[155,1019,1020],{"class":157,"line":250},[155,1021,1022],{},"  };\n",[155,1024,1025],{"class":157,"line":255},[155,1026,1027],{},"  const primary = servers[userRegion] || servers['us-east'];\n",[155,1029,1030],{"class":157,"line":261},[155,1031,1032],{},"  return [\n",[155,1034,1035],{"class":157,"line":267},[155,1036,1037],{},"    { urls: `stun:${primary}:3478` },\n",[155,1039,1040],{"class":157,"line":273},[155,1041,1042],{},"    {\n",[155,1044,1045],{"class":157,"line":278},[155,1046,1047],{},"      urls: [`turn:${primary}:3478`, `turns:${primary}:5349`],\n",[155,1049,1050],{"class":157,"line":284},[155,1051,1052],{},"      username: credentials.username,\n",[155,1054,1055],{"class":157,"line":290},[155,1056,1057],{},"      credential: credentials.password,\n",[155,1059,1060],{"class":157,"line":296},[155,1061,1062],{},"    },\n",[155,1064,1065],{"class":157,"line":302},[155,1066,1067],{},"  ];\n",[155,1069,1070],{"class":157,"line":308},[155,1071,1072],{},"}\n",[15,1074,1075],{},"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.",[19,1077,1079],{"id":1078},"operational-checks","Operational Checks",[15,1081,1082],{},"After deployment, validate with a TURN test client before sending production traffic:",[145,1084,1086],{"className":147,"code":1085,"language":149,"meta":150,"style":150},"# 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",[152,1087,1088,1093,1119,1123,1128],{"__ignoreMap":150},[155,1089,1090],{"class":157,"line":158},[155,1091,1092],{"class":161},"# Install turnutils (comes with coturn)\n",[155,1094,1095,1098,1101,1103,1105,1108,1111,1113,1116],{"class":157,"line":165},[155,1096,1097],{"class":168},"turnutils_uclient",[155,1099,1100],{"class":198}," -t",[155,1102,485],{"class":198},[155,1104,488],{"class":172},[155,1106,1107],{"class":198}," -w",[155,1109,1110],{"class":172}," secretpass",[155,1112,497],{"class":198},[155,1114,1115],{"class":198}," 3478",[155,1117,1118],{"class":172}," turn.example.com\n",[155,1120,1121],{"class":157,"line":179},[155,1122,183],{"emptyLinePlaceholder":182},[155,1124,1125],{"class":157,"line":186},[155,1126,1127],{"class":161},"# Check allocation success rate — should be 100%\n",[155,1129,1130],{"class":157,"line":192},[155,1131,1132],{"class":161},"# Check round-trip time — should be \u003C 5ms on same-region VPS\n",[15,1134,1135,1136,1139,1140,1143],{},"A healthy TURN server shows allocation latency under 5ms and zero auth failures in the coturn log. If you see ",[152,1137,1138],{},"401 Unauthorized"," in logs, check that your application server's shared secret matches the ",[152,1141,1142],{},"static-auth-secret"," in turnserver.conf exactly — whitespace differences cause silent mismatches.",[1145,1146,1147],"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 .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":150,"searchDepth":165,"depth":165,"links":1149},[1150,1151,1152,1153,1154,1155,1156,1157,1158],{"id":21,"depth":165,"text":22},{"id":54,"depth":165,"text":55},{"id":142,"depth":165,"text":143},{"id":208,"depth":165,"text":209},{"id":460,"depth":165,"text":461},{"id":607,"depth":165,"text":608},{"id":729,"depth":165,"text":730},{"id":973,"depth":165,"text":974},{"id":1078,"depth":165,"text":1079},"WebRTC","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1451187580459-43490279c0fa?w=1200&q=80","2025-08-01","Step-by-step coturn configuration for production WebRTC: TLS setup, credential management, quota enforcement, Prometheus metrics, and multi-region deployment patterns.","md",{},"\u002Fblog\u002Fwebrtc-turn-server-deployment",{"title":5,"description":1162},"blog\u002Fwebrtc-turn-server-deployment",[1169,1170,1171,1172,1173,1174],"coturn","turn-server","webrtc","ice","stun","nat-traversal","dlDvW-YjQBXQxhlzDlMzMCZAMHJUVFttTGWYmOgatm8",1776964996445]