WebRTC ICE Candidate Pooling and Privacy Timing
Separate ICE prefetch, candidate delivery and restart phases; a missing page event does not establish that earlier network activity did not occur.
BotBrowser Team
Want the structured docs for Fingerprint?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
The ICE candidate pool changes when some work may happen; it does not make gathering, page events, local description and a later restart the same observation. A quiet event log cannot establish that no earlier server-facing activity occurred. BotBrowser documents WebRTC address policies, but does not document a pool-size control or reversal of prior network activity.
TL;DR
- A nonzero
iceCandidatePoolSizerequests pre-gathering; it does not prove the pool is ready. setLocalDescription()is a candidate-delivery phase, not a timestamp for the first network activity.- The initial pool serves the first exchange;
restartIce()requests later negotiation and does not rewind earlier activity.
Watch the video
This English-language video follows the same question as the article: a WebRTC trace holds several clocks, so a configured pool size, a page candidate event, a local description and a server log cannot be swapped for one "ICE started" instant. It shows a controlled local fixture in Ubuntu Chrome, where a peer connection is built with a nonzero pool size and a later change returns InvalidModificationError, plus a second probe that counts candidate events inside a stated window and reads the gathering state. The count stays a page observation, not a server-side record, and the video does not claim that any real site was measured or that BotBrowser controls pool size. The audio is English.
Contents
- Watch the video
- Why timing changes the privacy question
- The pool has a bounded lifecycle
- Keep the observation layers separate
- Triage an incomplete timeline
- BotBrowser policy boundary
- Practical conclusion
BotBrowser can apply documented WebRTC address policies in controlled sessions, but its public documentation does not promise a candidate-pool-size control or reversal of earlier activity. The WebRTC leak-prevention guide explains candidate types and route policy; the mDNS candidate guide explains host-candidate representation. The narrower question here is when each ICE phase can be observed.
Why timing changes the privacy question
A WebRTC trace can contain several clocks: construction of the peer connection, optional candidate prefetch, a call to set a local description, candidate events delivered to the page, an offer/answer exchange, and a later ICE restart. Treating all of those as one instant called “ICE started” hides distinctions that matter when interpreting a privacy report. The WebRTC Recommendation defines iceCandidatePoolSize as the size of a prefetched pool and gives it a default of zero. A nonzero value requests a pool; it is not a timestamped record of what the page received.
| API boundary | Record | Do not infer |
|---|---|---|
RTCPeerConnection(configuration) | Requested iceCandidatePoolSize and capture start | That pre-gathering completed |
setLocalDescription() | Call time and delivered candidate events | That network activity began at that call |
restartIce() | Restart request and next negotiation | That the initial pool was restored |
The distinction matters because a page event describes an application-visible delivery point. It does not timestamp every DNS, STUN or TURN operation that might have happened earlier. The inverse also matters: if an application log contains no candidate event, that absence alone does not establish that no gathering or server-facing activity occurred. The capture could have started late, the application could have filtered its own record, or the observation could come from a different layer. Those possibilities are reasons to mark the evidence boundary, not permission to invent a missing event.
For an authorized compatibility or privacy review, keep four observations separate:
- The configured pool size and when that configuration was captured.
- Network activity seen by an independently authorized server-side record, if one exists.
- Candidate events delivered to the application, with the capture window stated.
- The local description and offer/answer phase recorded by the application.
These records answer different questions. A setting says what the application requested. A page event says what was delivered to script. A local description says what the application supplied to the negotiation API. A server record says what that particular server observed. None of these alone proves that an address policy preceded every relevant network action. The evidence should identify its clock and vantage point before two timestamps are compared.
The pool is also not a candidate count visible to the page. A configured size does not establish that the implementation finished gathering that number of usable candidates before a later call. RFC 9429 explicitly accounts for a pool that is depleted because more ICE components are needed or because gathering has not had enough time. In that case, remaining candidates are gathered normally. Therefore the configured value, the number of page events, and the contents of a local description are not interchangeable measurements.
This separation helps avoid two common overstatements. “No candidate event was logged before the local description” is a statement about a particular page log and its coverage. “No network activity happened before the local description” is a broader statement requiring evidence from the relevant network vantage point. Similarly, a nonzero pool setting permits prefetch behavior; it does not by itself prove that a particular server was contacted at a particular time. Record what each source can actually show and keep the rest as unknown.
The pool has a bounded lifecycle
The candidate pool has a defined role in negotiation; it is not a cache that is reused for every later exchange. RFC 9429 section 3.5.4 describes the sequence: when setLocalDescription begins the gathering needed for the first offer/answer exchange, the implementation should first check for candidates already available in the pool. Available candidates should be handed to the application through candidate events. If the pool is depleted because the exchange needs more components or the pool has not had enough time to gather candidates, the rest are gathered as usual. RFC 9429 says this pool behavior applies to the first offer/answer exchange; after that exchange the pool is emptied and is no longer used.
The RFC uses “SHOULD” for delivery of available pooled candidates. Preserve that modality: it is a standards recommendation, not evidence that a particular pool completed before a particular wall-clock time. The same section explains that holding pre-gathered candidates alive can consume resources on the configured STUN or TURN servers. That is a resource consequence stated by the RFC, not a measured traffic volume or a promise about retention duration in a specific browser build.
Configuration timing is constrained too. The WebRTC set-configuration algorithm rejects a changed iceCandidatePoolSize once setLocalDescription has already been called. The method reports InvalidModificationError for that restricted change, but the exception name is not a diagnosis: other configuration changes can produce the same exception. A useful incident record needs the old and requested configurations, the order of API calls, and the exception context. Without those, attributing the exception to pool size is only a guess.
| Phase | Pool interpretation | Application-visible evidence | What the record cannot establish |
|---|---|---|---|
| Construction with size 0 | This setting requested no prefetch | Initial configuration snapshot | That no later gathering will happen |
| Construction with nonzero size | Prefetch is permitted while candidates are prepared | Requested size; separate server observation if available | That the pool filled or a page event already fired |
| Before local description | The size can be selected or changed within API rules | Configuration snapshots and call order | That a requested pool is ready |
| First local-description gathering | Available pooled candidates should be delivered; shortages gather normally | Candidate events and local-description lifecycle | The complete earlier network timeline from page events alone |
| After the first exchange | The initial pool is empty and no longer used | Negotiation history | That a later restart recreates the prefetched pool |
| Later ICE restart | A new ICE generation is requested through negotiation | Restart call and subsequent signaling | That prior activity has been erased |
The matrix is an interpretation aid for records from an owned application, not a detector recipe. Capture phase labels and coarse ordering rather than candidate contents. Keep the distinction between a configuration request and a completed operation. If the capture has no independent network observation, say so; do not convert the page event stream into a proxy for server activity.
An ICE restart is a later negotiation action, not a rewind button. The restartIce() API requests an ICE restart for a subsequent negotiation. It does not retroactively remove an earlier event, erase a server log, or restore the initial prefetch pool after the first offer/answer exchange has consumed it. If a report spans more than one generation, label which phase each record covers. Otherwise, a candidate observed after restart can be incorrectly joined to evidence from the initial exchange.
Keep the observation layers separate
An evidence bundle should state which layer produced each observation and which time interval it covers. A configuration snapshot records the application's requested values. A page event records delivery to script. A local description records the data supplied at that API boundary. A server log, when the system owner is authorized to inspect it, records what that server observed. Their clocks may differ, their retention may differ, and their coverage may begin at different times. A comparison that omits these facts can look more conclusive than the underlying records warrant.
| Observation | It supports | It does not support by itself |
|---|---|---|
iceCandidatePoolSize: 0 before negotiation | That this setting did not request prefetch | That candidates cannot be gathered later |
| A nonzero configured pool | That prefetch was requested | That gathering completed or the pool remained available |
| A candidate event timestamp | That the page received that event at the recorded time | That the first network activity occurred then |
A setLocalDescription call | That the application entered the local-description phase | That all candidates had already been gathered |
A restartIce() call | That the application requested restart for subsequent negotiation | That the initial pool was restored or earlier activity erased |
| An absent event in a partial trace | That the trace does not contain that event | That no activity occurred outside the capture window |
A practical record can preserve the capture start time, ordered API phase markers, browser version, and whether the log is complete. The purpose is to interpret authorized diagnostics, not to collect more data than the diagnosis needs. Raw candidate strings, addresses, SDP bytes, server credentials and per-user identifiers are not necessary for the lifecycle distinctions in this article. If a separately authorized incident requires sensitive data, handle it under that system's retention and access policy rather than copying it into a general report.
Avoid joining a page event time to an assumed STUN request time when there is no independent network observation. Such a join may be useful as a hypothesis to investigate, but it is not an observed fact. The same caution applies to an export pipeline that drops fields: a field absent from an export can mean that the exporter omitted it, not that the browser never produced it. Preserve provenance for each record: source layer, capture interval, transformation steps and the point at which the record was retained.
For repeatable comparison, use the same observation definitions in both runs. If one run records API calls from connection construction and another starts logging at the first candidate callback, their apparent timelines are not aligned. Make a small table with the event definition, clock source, start boundary and missing-data rule before comparing outcomes. This is a review method for owned test systems; it does not require exposing candidate values or probing third-party endpoints.
Triage an incomplete timeline
When a report describes an address as “late,” define what “late” means before looking for a cause. It could mean the first candidate event seen by page code, the first candidate represented in the local description, the first event retained by an export process, or the first observation in an authorized server log. These timestamps are not interchangeable. First check whether capture began before the peer connection was constructed and whether the initial configuration was recorded. Then check whether the application had already called setLocalDescription when it attempted to change the pool size.
| Symptom | Check first | Avoid concluding |
|---|---|---|
| No candidate event before local description | Whether collection was requested and whether the capture covers earlier activity | That there was no prefetch or server contact |
| Fewer events than expected after local description | Whether the pool had enough capacity or preparation time; ordinary gathering may continue | That the connection failed solely because the pool was small |
InvalidModificationError during configuration | Which field changed and whether setLocalDescription had already been called | That pool size was necessarily the rejected field |
Events after restartIce() | The restart call, renegotiation order and capture completeness | That the prior generation or its activity disappeared |
| Server and page records disagree | Clock source, capture bounds, filtering and observation vantage | That either record describes the other layer |
This sequence keeps diagnosis tied to the evidence. Start with call order and capture scope. Compare the requested value with the value actually recorded before the local description. Separate configuration exceptions from event delivery. Only then compare page and server records if both were collected under authorization. If an item is missing, record the gap rather than filling it with an assumed default or a guessed event time.
If the initial configuration or capture interval is unavailable, mark the timeline incomplete. Do not reconstruct the original intent from a final candidate list. A final list is a retained result, not a complete chronology of how the implementation reached it. If the export has no provenance, say that the transformation history is unknown. This makes the report less absolute but more useful: another engineer can see which missing fact would change the interpretation.
When comparing two runs, differences may come from capture coverage, network response timing, application call order, or server logging boundaries, as well as the configured pool size. Record those conditions alongside the comparison. Do not claim that a pool-size change caused a difference unless the evidence isolates that change and the applicable source supports the proposed mechanism. The cited standards describe API behavior and lifecycle; they do not supply measurements for an unrun browser configuration.
The same method applies when a configuration change fails. Preserve the complete old and new configuration objects with secrets removed, note whether local description was already called, and retain the exact exception type and phase. Then consult the algorithm for the changed fields. InvalidModificationError tells you that the attempted change was not accepted under the API rules; it does not say which field was responsible without the surrounding record. A narrow conclusion avoids sending an engineer toward an unrelated network hypothesis.
BotBrowser policy boundary
BotBrowser's WebRTC documentation describes address-policy choices and configuration for managed sessions. It is the product source for those controls. The default pool value, its first-exchange lifecycle, and the timing of local-description processing come from the WebRTC Recommendation and JSEP. The public product documentation does not promise a dedicated pool-size control, that every candidate will be prefetched, or that changing a policy later removes earlier server-side activity.
For a controlled comparison, record the selected product policy separately from API phase evidence and attribute each statement to its source. Do not treat a configuration reference as a network-side proof. Do not treat a missing page event as proof that the host never contacted a configured service. The WebRTC leak-prevention guide remains the operational reference for address-policy setup; the lifecycle tables here add an interpretation layer for time and evidence scope. No result about an unexecuted browser build, network path, or deployment is claimed.
FAQ
Does a nonzero pool size guarantee candidates are ready before negotiation?
No. It requests pre-gathering, but RFC 9429 describes cases where the pool is depleted or gathering has not had enough time. Available pooled candidates should be delivered as gathering begins; the setting is not a completion receipt.
Can changing the pool size after local description fix an earlier capture?
No. The W3C set-configuration algorithm restricts that change after setLocalDescription() has been called. A later configuration cannot rewrite a previous event log or server observation.
Does restartIce() restore the initial pool?
No. It requests an ICE restart for a subsequent negotiation. The restart has its own evidence interval and does not make earlier network activity disappear.
Sources
- W3C WebRTC Recommendation: RTCConfiguration and set-configuration algorithm
- RFC 9429, section 3.5.4: ICE Candidate Pool
- MDN: RTCPeerConnection.setConfiguration()
- MDN: RTCPeerConnection.restartIce()
- BotBrowser WebRTC leak-prevention documentation
Practical conclusion
Keep configuration, page events and server observations as separate evidence. Preserve the capture window and call order; when either is missing, report the timeline as incomplete instead of inferring that no earlier network activity occurred.
This article is part of the guide: What Is a WebRTC IP Leak? Prevention and Browser Privacy
Related Articles
What Is a WebRTC IP Leak? Prevention and Browser Privacy
NetworkWebRTC mDNS Candidates and Local-Network Privacy
NetworkWebRTC Network Identity and Privacy: Choosing the Right Policy
NetworkProxy, DNS, and WebRTC Consistency for a Coherent Browser Identity
FingerprintWhat Private Browsing Does and Does Not Do
FingerprintWebRTC Codec Fingerprinting: Media Capabilities Leak
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.