Rethinking the Fork: Meta’s Dual-Stack WebRTC Upgrade Path

Meta’s real-time communication (RTC) stack—spanning Messenger, Instagram video, Cloud Gaming, and Meta Quest casting—relies on a heavily customized variant of the open-source WebRTC library. For years, that meant maintaining a permanent internal fork. The pitfalls of that strategy are well known: as upstream evolves and internal features accumulate, the cost of merging external commits grows until the fork becomes effectively frozen, cut off from community fixes and improvements.

Recently, Meta completed a multiyear migration to escape that trap. The engineering team moved over 50 use cases from the divergent fork onto a modular architecture built on the latest upstream WebRTC, using it as a foundation while injecting proprietary implementations of key components. This article details the technical engineering behind that transition: building two WebRTC versions simultaneously within a single library for A/B testing, managing the constraints of a monorepo, and establishing a continuous upgrade cycle against upstream.

The Core Problem: Monorepo Constraints and the Linker

WebRTC upgrades carry real risk when serving billions of users across diverse devices. A one-time, hard cutover isn’t feasible—regressions would be difficult to roll back and could degrade experiences unpredictably. The team needed A/B testing: run the legacy WebRTC version alongside the new upstream version, with clean patches applied, in the same app, and dynamically switch users between them for verification.

Two technical hurdles shaped the solution. First, application build graph and size constraints pushed toward statically linking both WebRTC versions. But that violates the C++ linker’s One Definition Rule (ODR), producing thousands of symbol collisions. Second, Meta’s monorepo has no widespread support for feature branches, so the team needed a mechanism to maintain custom patches for open-source projects, pull new upstream versions, and rebase patches repeatedly—without repeating the whole migration process each cycle.

Building the Dual-Stack Architecture

The solution centers on a shim layer: a proxy library that sits between application code and the underlying WebRTC implementations. Instead of apps calling WebRTC directly, they call a unified, version-agnostic shim API. The shim holds a “flavor” configuration and dispatches each call at runtime to either the legacy or latest WebRTC implementation.

Shimming at the lowest possible layer was a deliberate choice to minimize binary size regression. Duplicating the higher-layer call orchestration library would have added roughly 38 MB (uncompressed). The shim approach adds only about 5 MB—an 87% reduction, while preserving A/B testing capability.

Resolving Symbol Collisions

Statically linking two WebRTC copies into one binary generates thousands of duplicate symbol errors. The team solved this with automated renamespacing: scripts systematically rewrite every C++ namespace per version, so webrtc:: in the latest upstream copy becomes webrtc_latest::, and the legacy copy becomes webrtc_legacy::. This applied across all external namespaces in the library.

Not everything in WebRTC lives in a namespace, though. Global C functions, free variables, and classes left outside namespaces also collide. Those were moved into namespaces where possible, or their symbols were manipulated with flavor-specific identifiers for the rest.

Macros and preprocessor flags presented a subtler problem. Macros like RTC_CHECK and RTC_LOG are often used outside WebRTC in wrapper libraries. Including headers from both versions in the same translation unit triggers redefinition errors. The solution combined three strategies:

  • Removing spurious includes
  • Renaming rarely-used macros
  • Sharing internal WebRTC modules across versions where viable (e.g., rtc_base), which also cut binary size and reduced the shimming surface area

Preserving Backward Compatibility

Renamespacing every symbol in WebRTC would break existing call sites that weren’t moving to dual-stack. The initial approach—forward-declaring every used symbol from the new namespace and wiring it to the old one—worked but produced a large, fragile header requiring heavy maintenance.

The refined solution uses bulk namespace imports via C++ using declarations. Importing an entire flavor namespace into the familiar webrtc:: namespace yields a concise declaration header where new symbols are handled automatically. Since these are pure compiler directives, there’s no binary size impact. External engineers continue writing code exactly as before; migration happens in parallel only for the call sites that matter.

Runtime Flavor Dispatch

With the shim wrapping both versions, the next issue was dispatch mechanics. Each adapter and converter must instantiate the correct underlying object—webrtc_legacy:: or webrtc_latest::—based on a globally configured flag.

A template-based helper library centralizes shared logic, with version-specific behavior expressed through C++ template specializations. This avoids code duplication while supporting backward-compatible single-flavor builds during the transition period. A global flavor enum, set early in each app’s startup, determines which version activates. Directional adapters implement the unified API and dispatch to the underlying WebRTC object (or vice versa). Directional converters translate structs and enums between the shim and WebRTC type systems.

Automating Shim Generation

Manually writing adapters and converters across dozens of APIs—each requiring an abstract API definition, implementations, and unit tests—would have been prohibitively slow. Using abstract syntax tree (AST) parsing, the team built a code generation system that produces baseline shim code for classes, structs, enums, and constants. The output is fully unit-tested and easy to extend.

This automation dramatically increased velocity, from roughly one shim per day to three or four per day, while reducing human error. For simple shims where APIs are identical across versions, generated code needed near-zero manual intervention. Complex cases—API discrepancies, factory patterns, static methods, raw pointer semantics, ownership transfers—still required engineers to refine the generated baseline.

Rewiring the Application Layer

With the shim in place, the team rewired application references from direct WebRTC types to shim equivalents (e.g., webrtc::Foo became webrtc_shim::Foo). This introduced ownership complexities and subtle bugs around null handling and memory management, mitigated through comprehensive unit testing of ownership transfer scenarios and end-to-end testing for risky changes.

Some internal components injected into WebRTC from outside posed a particular challenge due to deep dependencies on WebRTC internals. Since shimming those would mean proxying WebRTC against itself, the team “duplicated” them using C++ macro and build machinery: dynamically changing namespaces at build time, duplicating the high-level build target, and exposing symbols for both flavors through a single header.

The migration touched over 10,000 lines of new shim code and modified hundreds of thousands of lines across thousands of files. Despite the scope, careful testing and review meant no major issues. The shim approach remains in active use today for continuous upgrades, enabling A/B testing of each new upstream release before full rollout.

Tracking Patches Without Feature Branches

The second major challenge was managing custom patches in a monorepo environment that doesn’t support widespread branching. The clear requirement: each patch must have a delineated purpose and an owning team.

Two options were considered: tracking patch files checked into source control and reapplying them in order, or tracking patches in a separate repository that supports branching. The team chose the latter—feature branches in a separate Git repository.

This approach offered a clean pipeline for upstream contributions. By basing branches on the libwebrtc Git repo, the team could reuse existing Chromium tooling (gn, gclient, git cl) for building, testing, and submitting.

For each upstream Chromium release (e.g., M143, tagged 7499 in git), a base/7499 branch is created. Each patch (e.g., “debug-tools”) gets its own branch (debug-tools/7499) on top of that base commit. During version upgrades, feature branches merge forward—debug-tools/7499 into debug-tools/7559, hw-av1-fixes/7499 into hw-av1-fixes/7599, and so on. Once all features are merged forward with conflicts resolved and builds and tests passing, the feature branches merge sequentially to produce the release candidate branch (r7559).

This strategy is highly parallelizable across branches, preserves all Git history and context, and is well-positioned for future improvements like LLM-driven auto-resolution of merge conflicts. It also makes each feature branch easy to submit as a whole as an upstream contribution to OSS.

Living at Head: The Payoff

Shipping a binary with both the legacy and modern WebRTC stacks enabled the dual-stack transition. After launching webrtc/latest on version M120, the team has advanced to M145, staying aligned with current stable Chromium releases rather than lagging years behind. Upstream upgrades are now ingested immediately.

Measured Gains

  • Performance: CPU usage dropped by up to 10%, with crash rates improving by up to 3% across major apps.
  • Binary Size: The modern upstream version is more efficient, trimming 100-200 KB (compressed) depending on the app.
  • Security: Deprecated libraries such as usrsctp were eliminated, and vulnerabilities in the legacy stack were patched.
  • These improvements translated into observable user engagement gains while running on a modern foundation.

The project demonstrates that a complex monorepo riddled with constraints does not demand a full rewrite to escape the forking trap. The shim layer and dual-stack approach serve as a blueprint for any organization looking to modernize entrenched technical debt incrementally.

Automating Upkeep with AI

With the migration complete, the team now "lives at head" but still maintains internal patches over upstream. To keep this sustainable, they are building automation into their workflows:

  1. Build Health: Agents are being developed to automatically address build errors across Git branches.
  2. Conflict Resolution: Rebasing internal patches on new WebRTC releases regularly surfaces merge conflicts. AI agents are being trained to resolve the majority automatically, leaving only complex architectural changes for human engineers.

Acknowledgements

This effort was driven by a small engineering team that championed the strategic project despite its complexity. Their creativity, problem-solving, and persistence carried it through unexpected blockers: Dor Hen, Guy Hershenbaum, Jared Siskin, Liad Rubin, Tal Benesh, and Yosef Twaik.