Jitsi Meet is the open-source WebRTC video conferencing platform behind a huge number of self-hosted deployments and managed services. Out of the box it tries peer-to-peer connections first and falls back to the Jitsi Videobridge (JVB) for larger calls. Both paths work great until a user joins from a restrictive network: cellular data, a corporate VPN, a hotel Wi-Fi, or anything else that blocks the ports WebRTC needs. At that point you need a TURN server, and how you wire it into Jitsi Meet determines whether those users connect or stare at a black screen. This guide walks through every step of configuring TURN for Jitsi Meet, from the simplest static config to advanced setups.

What Is Jitsi Meet?

Jitsi Meet is a fully open-source WebRTC video conferencing stack. It powers meet.jit.si, thousands of self-hosted deployments, and several managed providers. The architecture has four main components that matter when you're configuring TURN:

  • Prosody: The XMPP server that handles signaling between clients. Prosody is also where TURN credentials are typically issued.
  • Jicofo: The Jitsi Conference Focus, which manages conference state and assigns clients to a videobridge.
  • JVB (Jitsi Videobridge): The media router that mixes audio and video for calls with more than two participants.
  • The web client: The browser-side Jitsi Meet UI, configured via config.js.

When a client cannot reach JVB directly, or two clients in a peer-to-peer call cannot reach each other, the connection has to be relayed through a TURN server. That is the missing piece this guide adds to a default Jitsi installation.

Why Jitsi Meet Needs a TURN Server

Jitsi tries direct paths first: peer-to-peer for two-party calls, JVB for multi-party calls. Both paths assume the client can reach the remote endpoint over UDP on the assigned ports. In the real world, that assumption breaks for a meaningful percentage of users:

  • Carrier-grade NAT (CGNAT): Most mobile carriers run symmetric NAT, where the STUN-discovered address is useless to any peer other than the STUN server itself.
  • Corporate firewalls: Many enterprise networks block outbound UDP entirely as a security policy.
  • Public Wi-Fi: Hotels, hospitals, coffee shops, and conference venues often allow only HTTP and HTTPS through the firewall.
  • Locked-down guest networks: Restrictive networks often allow only port 443 outbound, blocking the standard WebRTC media ports.

Industry estimates put TURN-relayed traffic at around 10 to 25% of WebRTC connections overall, and Jitsi's own documentation cites estimates as high as 40% for video conferencing specifically. Without a TURN server, that slice of your users gets a call that never connects, and your support inbox fills up with "doesn't work on my phone" tickets.

Option 1: External TURN via config.js

The simplest way to wire an external TURN server into Jitsi Meet is by editing the web client's config.js directly. This works for both the JVB-relayed mode and the peer-to-peer mode, and it requires no server restart.

Step 1: SSH into your Jitsi server

Connect to the server where Jitsi Meet is installed using your SSH credentials.

Step 2: Open the config.js file

Jitsi's web config lives at /etc/jitsi/meet/<your-domain>-config.js. Open it with your editor of choice:

sudo nano /etc/jitsi/meet/meet.example.com-config.js

Replace meet.example.com with the domain you used during your Jitsi install.

Step 3: Add the TURN server entries

Find the stunServers array (despite the name, it accepts both STUN and TURN entries). Replace or extend it with your TURN credentials, listing all three transports so ICE can pick the one that works on each user's network:

useStunTurn: true,

stunServers: [
    { urls: 'stun:stun.expressturn.com:3478' },
    {
        urls: 'turn:relay1.expressturn.com:3478?transport=udp',
        username: 'YOUR_TURN_USERNAME',
        credential: 'YOUR_TURN_PASSWORD'
    },
    {
        urls: 'turn:relay1.expressturn.com:3478?transport=tcp',
        username: 'YOUR_TURN_USERNAME',
        credential: 'YOUR_TURN_PASSWORD'
    },
    {
        urls: 'turns:relay1.expressturn.com:443?transport=tcp',
        username: 'YOUR_TURN_USERNAME',
        credential: 'YOUR_TURN_PASSWORD'
    }
],

p2p: {
    enabled: true,
    useStunTurn: true,
    stunServers: [
        { urls: 'stun:stun.expressturn.com:3478' },
        {
            urls: 'turn:relay1.expressturn.com:3478?transport=udp',
            username: 'YOUR_TURN_USERNAME',
            credential: 'YOUR_TURN_PASSWORD'
        },
        {
            urls: 'turn:relay1.expressturn.com:3478?transport=tcp',
            username: 'YOUR_TURN_USERNAME',
            credential: 'YOUR_TURN_PASSWORD'
        },
        {
            urls: 'turns:relay1.expressturn.com:443?transport=tcp',
            username: 'YOUR_TURN_USERNAME',
            credential: 'YOUR_TURN_PASSWORD'
        }
    ]
}

Save the file. No service restart is needed for config.js changes; the browser fetches a fresh copy on each page load.

Step 4: Verify in the browser

Reload the Jitsi Meet web page, join a meeting, and confirm the new TURN config is loaded. The next section covers how to verify TURN is actually being used.

Option 2: XMPP-Managed Credentials via Prosody

The config.js approach works, but it bakes long-lived TURN credentials into JavaScript that anyone can view in browser DevTools. For production deployments, the better pattern is to let Prosody hand out fresh credentials per session using XEP-0215 (External Service Discovery).

Jitsi's web client supports this out of the box via the mod_external_services Prosody module. When configured, Jitsi requests TURN credentials from Prosody at the start of each conference, and Prosody returns time-limited credentials that the client uses immediately.

Step 1: Edit the Prosody config

Open your Prosody configuration file. The path varies by install, but it is typically at:

sudo nano /etc/prosody/conf.avail/meet.example.com.cfg.lua

Step 2: Enable mod_external_services

Make sure the module is loaded. In the main modules_enabled block:

modules_enabled = {
    -- ...existing modules...
    "external_services";
}

Step 3: Configure your TURN entries

Add an external_services block with your STUN and TURN endpoints:

external_services = {
    {
        type = "stun",
        host = "stun.expressturn.com",
        port = 3478,
        transport = "udp"
    },
    {
        type = "turn",
        host = "relay1.expressturn.com",
        port = 3478,
        transport = "udp",
        username = "YOUR_TURN_USERNAME",
        password = "YOUR_TURN_PASSWORD"
    },
    {
        type = "turn",
        host = "relay1.expressturn.com",
        port = 3478,
        transport = "tcp",
        username = "YOUR_TURN_USERNAME",
        password = "YOUR_TURN_PASSWORD"
    },
    {
        type = "turns",
        host = "relay1.expressturn.com",
        port = 443,
        transport = "tcp",
        username = "YOUR_TURN_USERNAME",
        password = "YOUR_TURN_PASSWORD"
    }
}

Step 4: Reload Prosody

Apply the new config:

sudo prosodyctl reload

The Jitsi web client will start requesting these endpoints automatically on the next conference. No config.js edits are needed because the client discovers the servers via XMPP.

Routing TURN Through Port 443

Standard TURN ports are 3478 (UDP and TCP) and 5349 (TLS). Some corporate and guest networks block everything except 443. To reach those users, you can route TURN/TLS traffic through port 443 alongside your HTTPS site using nginx's stream module with SNI-based routing.

If you are using a managed TURN provider that already offers a 443 endpoint (for example turns:relay1.expressturn.com:443), this step is unnecessary. The provider handles it.

If you are self-hosting coturn, add a stream block to your nginx config:

stream {
    map $ssl_preread_server_name $upstream {
        meet.example.com web_backend;
        turn.example.com turn_backend;
    }

    upstream web_backend {
        server 127.0.0.1:4444;
    }

    upstream turn_backend {
        server YOUR_TURN_SERVER_IP:5349;
    }

    server {
        listen 443;
        listen [::]:443;
        ssl_preread on;
        proxy_pass $upstream;
        proxy_buffer_size 10m;
    }
}

This makes both your Jitsi web UI and your TURN/TLS endpoint available on port 443. nginx inspects the SNI server name in the TLS handshake and routes accordingly. You then need to move your existing HTTPS server block to a different port (typically 4444) so nginx's stream module can claim 443. Full nginx config details are in the Jitsi handbook's TURN section.

Testing Your TURN Configuration

After configuration, verify that TURN is actually being used. Without verification it is easy to think you are protected when in reality every test call has been finding a direct path on your home network.

Force-relay in config.js

The fastest verification is to force every connection through TURN by setting the ICE transport policy in your local browser. Add this to your config.js temporarily:

iceTransportPolicy: 'relay'

Now every connection has to use TURN or fail. If calls still work, your TURN config is correct. If they fail, fix it before users hit the same problem.

Inspect chrome://webrtc-internals

Open chrome://webrtc-internals in a new tab while a Jitsi call is connected. Find the active RTCPeerConnection and look for the selected candidate pair. With force-relay enabled, the type should be relay (TURN-mediated). Without force-relay, you might see host (LAN), srflx (STUN-discovered), or relay depending on the network path. Anything other than host on a LAN test is a good sign.

Test from a real mobile network

Wi-Fi testing on your dev LAN will not exercise the CGNAT case. Turn off Wi-Fi on a real phone and join a call from cellular data. If TURN is wired up correctly the call connects; if not, it fails the same way it would for your users.

Bandwidth Considerations

Jitsi's bandwidth profile is different from a typical 1:1 WebRTC app. P2P mode handles 2-person calls cheaply, but most production traffic goes through JVB, which means TURN traffic for restricted-network users is JVB-to-client (and back) rather than peer-to-peer. Some rough numbers:

  • A 2-person 720p P2P call relayed: roughly 675 MB per relayed hour, combined both directions.
  • A 4-person JVB-bridged call where one participant needs TURN: roughly 1 to 2 GB per relayed hour, depending on simulcast settings and the bridge's send bitrate.
  • An audio-only call relayed: roughly 30 MB per hour.

Jitsi's documentation suggests around 40% of conference participants end up needing TURN in many deployments. For a Jitsi instance hosting a few hundred meeting-hours per month, that comes out to a few hundred GB at most. The free tier on most managed TURN services covers it. Once you cross 1 TB of relayed traffic per month, flat-pricing providers like ExpressTURN ($9/month for 5 TB) become significantly cheaper than metered providers or AWS bandwidth on a self-hosted coturn.

Best Practices for Jitsi TURN

  • List multiple transports: UDP on 3478, TCP on 3478, TLS on 443. ICE picks whichever works on each user's network.
  • Use XMPP-managed credentials in production: Static credentials in config.js are fine for testing, but Prosody's mod_external_services gives each session a fresh credential and avoids exposing long-lived secrets to the browser.
  • Cover port 443: Whether through a managed provider's TLS endpoint or by routing self-hosted coturn through nginx, port 443 access is what rescues the most restrictive networks.
  • Test from real mobile data: Wi-Fi will not reproduce CGNAT. Cellular testing is the only way to confirm your TURN config works.
  • Monitor TURN bandwidth: Unexpected spikes indicate either a misbehaving client, abuse, or a sudden shift in your user network mix. Both managed providers and self-hosted coturn give you usage logs.
  • Decide self-hosted vs managed early: Self-hosted coturn is free but adds operational overhead (bandwidth costs, monitoring, upgrades). Managed providers like ExpressTURN handle the infrastructure for a predictable monthly cost.

Wrapping Up

Jitsi Meet ships with sensible defaults, but a default Jitsi install will fail for a meaningful slice of users until you wire up TURN. The config.js path is the fastest way to get there. The Prosody path is the right pattern for production. Either way, listing UDP, TCP, and TLS-443 transports and verifying with chrome://webrtc-internals is what turns a fragile Jitsi deployment into one that works on every network type your users will throw at it.