Most product teams underestimate what it actually takes to ship reliable in-app calling. They see video-call SDKs advertised as "drop-in" and assume they'll have something working in a sprint. What they don't account for is NAT traversal, signaling architecture, codec negotiation, the native OS integration requirements on iOS and Android, and — most critically — what happens to the call when a user's phone switches from LTE to WiFi mid-conversation. The gap between a demo that works on a stable office network and a production feature that holds up for real users in real conditions is wider than almost any other area of mobile engineering.
What WebRTC Actually Is and Why It's the Right Foundation
WebRTC (Web Real-Time Communication) is an open standard, originally developed at Google, that enables peer-to-peer audio, video, and data transfer directly between browsers and native applications. It's baked into every major browser and available on iOS and Android via native libraries (Google's libwebrtc, or wrappers like flutter_webrtc and React Native's react-native-webrtc).
The reason WebRTC has become the default for in-app calling is pragmatic: the codec support (Opus for audio, VP8/VP9/H.264 for video), adaptive bitrate control, and built-in congestion handling are production-grade and constantly maintained. You're not writing low-level packet handling — you're writing signaling logic and UI on top of an engine that already knows how to degrade gracefully under packet loss.
What WebRTC does not handle for you is signaling — the exchange of session descriptions and ICE candidates that lets two peers find each other. That's your responsibility to build or buy.
The Signaling Layer: Your Biggest Architecture Decision
Before any audio or video can flow between two users, they need to exchange a Session Description Protocol (SDP) offer and answer, plus ICE candidates that describe their network addresses. This exchange happens through your signaling server — a channel you control, completely separate from the media stream itself.
Common signaling implementations include:
- WebSocket server: The most common approach. Low-latency, bidirectional, and straightforward to implement with Node.js (using
wsor Socket.IO) or Python (asyncio + websockets). Your server receives and routes messages between peers but never touches the media. - Firebase Realtime Database or Firestore: Works well for smaller-scale apps and removes the need to manage a signaling server. Both parties write SDP and ICE data to known document paths. The downside is slightly higher signaling latency compared to a raw WebSocket.
- SIP over WebSocket: For enterprise telephony or apps that need to interoperate with PSTN (real phone networks), SIP is the signaling protocol to use. This adds complexity but is sometimes non-negotiable for healthcare, real estate, or enterprise apps.
Whichever approach you choose, your signaling server needs to handle: session creation, routing signals to the correct peer, room membership, and — critically — cleanup when a peer disconnects unexpectedly. ICE candidate leaks and orphaned sessions are the most common cause of calls that connect on one side but not the other.
NAT Traversal: STUN, TURN, and Why You Need Both
The majority of mobile devices sit behind NAT (Network Address Translation) — they have a private IP address that the internet can't directly reach. WebRTC uses the ICE framework to find a path between two peers. ICE tries candidates in order: direct (host), STUN-assisted (server-reflexive), and TURN-relayed.
A STUN server lets a device discover its public IP address. When both peers are on different networks but have reachable public IPs, ICE can establish a direct peer-to-peer path using STUN. This is fast and cheap — STUN servers are stateless and Google provides public ones (stun:stun.l.google.com:19302).
TURN (Traversal Using Relays around NAT) is the fallback when direct connection is impossible — typically when both peers are behind symmetric NATs, or when corporate firewalls block UDP. TURN relays all media through a server you operate. This solves the connectivity problem but adds latency and bandwidth cost. In practice, 15–25% of real-world calls need TURN relay, depending on your user base's network environments. Skipping TURN means those calls simply fail silently.
Running your own TURN server using Coturn is not complicated, but capacity planning matters. A TURN server relaying a 100 kbps audio call uses 100 kbps of server bandwidth per direction. At scale, TURN infrastructure becomes a meaningful line item. Services like Twilio Network Traversal Service or Metered.ca provide TURN as a managed offering if self-hosting isn't worth the overhead.
Native OS Integration: CallKit on iOS, ConnectionService on Android
A WebRTC call that appears as a generic app notification rather than a native call screen is a second-class experience. Users expect in-app calls to behave like phone calls — showing up on the lock screen, allowing answer/decline from the notification, pausing music, and integrating with Bluetooth headsets.
On iOS, this means integrating CallKit. CallKit is Apple's framework for VoIP call management. It lets your app report incoming calls to the system so they appear as full-screen call interfaces, even when the app is backgrounded or the phone is locked. CallKit also handles audio session management — handing control to your WebRTC audio layer when the call is answered and returning it to the system when it ends.
The iOS wrinkle: CallKit requires a PushKit VoIP push token (separate from regular APNs) for incoming calls when the app is backgrounded. Regular background push has too high a delivery latency for call ringing. PushKit delivers with high priority and wakes the app in the background specifically to handle the incoming call — but Apple's policy now mandates that every PushKit push must result in a CallKit call report, or the app risks App Store rejection.
On Android, the equivalent is ConnectionService, which integrates with the system's telecom manager. For apps targeting Android 10+, you also need to handle the MANAGE_OWN_CALLS permission. Many teams use a high-priority FCM notification as the incoming call trigger on Android, which is simpler than ConnectionService but delivers a less native experience.
Chat Architecture Alongside Calling
In-app chat is typically built on a separate channel from voice/video, though WebRTC's data channels can carry text messages over the same peer-to-peer connection. The trade-off:
| Approach | Best For | Trade-offs |
|---|---|---|
| WebRTC Data Channel | Real-time chat during an active call, no server storage | Messages lost if connection drops; no history; both peers must be online |
| WebSocket + server persistence | Persistent chat, message history, offline delivery | Requires server infrastructure and storage |
| Third-party SDK (Stream, Sendbird) | Fast time-to-market, managed infrastructure | Recurring per-user cost; less control over data residency |
For most consumer-facing apps, a WebSocket-based chat with server-side persistence is the right call. Messages are stored, deliverable to offline users via push notification, and queryable for history. The WebRTC data channel is a useful complement for ephemeral in-call messaging (like cursor sharing in a collaborative tool) but shouldn't be your only chat channel.
Handling Poor Network Conditions
Mobile networks are unreliable by nature. The WebRTC engine handles many things automatically — adaptive bitrate, jitter buffering, packet loss concealment for audio — but your application layer needs to handle the cases above the engine:
- ICE restart: When a user's network changes (LTE to WiFi), the existing ICE candidates become invalid. WebRTC supports ICE restarts — your signaling layer exchanges new candidates over the existing signaling channel and the call continues. This requires your signaling connection to survive the network change, which means implementing reconnection logic in your WebSocket client.
- Reconnection UI: Show the user what's happening. "Reconnecting…" with a timer is better than silence followed by a dropped call notification.
- Codec selection: For voice-only calls on poor networks, Opus at 16–24 kbps is the right target. Avoid defaulting to the highest-quality codec — it wastes bandwidth that constrained networks can't sustain.
- Connection timeout handling: Define explicit timeouts for ICE gathering and connection establishment. Without them, the app can appear frozen while silently retrying for 30+ seconds.
Frequently Asked Questions
Do I need a media server for WebRTC calls?
For one-to-one calls, no — WebRTC is peer-to-peer and media flows directly between devices (or through a TURN relay). For group calls with more than two participants, a Selective Forwarding Unit (SFU) like mediasoup, Janus, or Jitsi is typically needed. Without an SFU, each participant would need to upload their stream to every other participant simultaneously, which becomes unworkable beyond 3–4 people on mobile connections.
Is WebRTC secure?
Yes — DTLS (Datagram Transport Layer Security) and SRTP (Secure Real-time Transport Protocol) are mandatory in the WebRTC specification. All media is encrypted in transit. Your signaling channel is your responsibility to secure — use WSS (WebSocket Secure) and authenticate signaling connections against your user sessions.
What's the difference between WebRTC and SIP/VoIP?
SIP is a signaling protocol originally designed for telephony, capable of interoperating with the PSTN (public phone network). WebRTC is a broader standard focused on browser and app-based real-time communication, with its own signaling flexibility. Many enterprise apps use SIP over WebRTC transport — SIP for call control and WebRTC for the media path — to get PSTN interoperability with modern browser/app reach.
How do I handle the incoming call when the app is killed on iOS?
This requires PushKit VoIP pushes, not regular APNs. Your server sends a VoIP push to the device, iOS wakes the app in the background (even if killed), and your app reports the incoming call to CallKit, which displays the native call screen. This must happen within a few seconds of receiving the push — any significant async work before the CallKit report will cause the system to terminate the app.
Mexilet Technologies supports teams on exactly this kind of work through our mobile app development and software development team.
Building in-app calling and chat that holds up in production — across iOS and Android, through unreliable networks, with proper native OS integration — is one of the more demanding mobile engineering challenges. Mexilet Technologies has built this infrastructure for real estate, support, and enterprise apps, and works as a backend development partner for software teams who need this done without burning months of internal engineering time. Start a conversation about what your app needs.
