Segmenting RUM Data by Effective Connection Type

navigator.connection.effectiveType is the only network signal a browser will give you, and it is widely misunderstood: it is not the connection technology, not the current throughput, and not stable within a session. Used carefully it explains a large share of load-metric variance and justifies adaptive loading. Used carelessly it produces cohorts that mean nothing. This guide sits under Device & Network Segmentation and covers what the value actually represents, how to capture it so the label matches the measurement, and which product decisions it can support.

Prerequisites

  • A beacon pipeline accepting an extra low-cardinality field, as in Self-Hosted Beacon Collection.
  • A device tier already on the beacon, from Classifying Device Tiers in RUM Beacons — network and device cross-cut, and neither alone is enough.
  • Acceptance that this is Chromium-only. Safari and Firefox expose nothing, so a large share of traffic will be unknown.

How to capture and use it

Step 1 — Know what the value means

effectiveType is a bucketed estimate derived from recent round-trip time and throughput observations, mapped onto four labels.

Value Roughly corresponds to What it does not mean
slow-2g RTT above ~2000 ms The device is on a 2G radio
2g RTT ~1400 ms, ~70 kbps Anything about the carrier
3g RTT ~270 ms, ~700 kbps A 3G connection specifically
4g RTT below ~270 ms Fast — it is the top bucket, so it includes everything above the threshold

The last row is the one that trips people up: 4g is an open-ended top bucket that contains fibre, good wifi, 5G and a mediocre LTE connection alike. Most of your traffic will be in it, and it will not discriminate at all within that group.

Two companion properties are worth reading at the same time: saveData, an explicit user preference for reduced data usage, and rtt/downlink, coarse rounded estimates that occasionally add resolution inside the 4g bucket.

Step 2 — Read it once, at the start of the page load

// network.js — one read, frozen for the life of the page view.
export function networkCohort() {
  const c = navigator.connection;
  if (!c) return { network: 'unknown', saveData: false, rtt: null };
  return Object.freeze({
    network: c.saveData ? 'save-data' : (c.effectiveType || 'unknown'),
    saveData: Boolean(c.saveData),
    // Rounded to 25 ms by the browser; useful only in aggregate.
    rtt: typeof c.rtt === 'number' ? c.rtt : null,
    downlinkMbps: typeof c.downlink === 'number' ? c.downlink : null,
  });
}

Reading it once is the important part. The estimate updates during the session as the browser observes more traffic, so a metric captured at 400 ms and labelled with a value read at 8 s may be tagged with a network the page never actually used.

Step 3 — Cross-cut with device tier before drawing conclusions

Network and device are independent constraints, and the interesting cells are the corners.

What each combination is telling youA high-tier device on a slow connection and a low-tier device on a fast one both land in the Poor band, for opposite reasons and with opposite fixes.What each combination is telling youp75 LCP by device tier and connection estimate4g3g2g or slowerhigh1.6 s2.9 s5.4 smid2.7 s4.1 s7.2 slow4.3 s6.0 s9.8 s
A high-tier device on a slow connection and a low-tier device on a fast one both land in the Poor band, for opposite reasons and with opposite fixes.

Reading the grid: moving down a column is CPU cost, moving right along a row is bandwidth cost. If your worst cell is bottom-left rather than top-right, your problem is main-thread work and no amount of image optimisation will fix it.

Step 4 — Use it for adaptive loading, carefully

The signal is good enough to justify serving less to constrained sessions, provided the degradation is graceful and the decision is made once.

// adaptive.js — decide once, apply consistently, never mid-session.
const { network, saveData } = networkCohort();
const constrained = saveData || network === '2g' || network === 'slow-2g';

export const loadingPolicy = Object.freeze({
  // Skip speculative work entirely on constrained connections.
  prefetchRoutes: !constrained && network === '4g',
  // Fewer, smaller images; the format guidance is in the LCP material.
  imageQuality: constrained ? 'low' : 'default',
  // Non-essential widgets wait for an explicit interaction.
  deferWidgets: constrained,
  // Video posters instead of autoplaying video.
  autoplay: !constrained && network === '4g',
});

Two rules keep this honest. Degrade the experience, never remove functionality — a user on a slow connection still needs to be able to buy the thing. And record the policy applied on the beacon, or you will later be comparing two different products and calling it a performance difference.

Step 5 — Report the policy alongside the metric

queueBeacon({
  metric: 'LCP',
  value: Math.round(lcp),
  network,
  saveData,
  deviceTier,
  // Which variant of the page this session actually received.
  policy: constrained ? 'reduced' : 'full',
});

Without the policy field, the reduced-experience sessions look artificially fast and your adaptive loading appears to have improved the network cohort far more than it did.

Effect of adaptive loading on the constrained cohortA 50% improvement on a cohort that was never going to reach Good. That is still the right work: the cohort is small, and its experience was unusable.Effect of adaptive loading on the constrained cohortp75 LCP for sessions reporting 2g, slow-2g or Save-DataBefore: full experience7,200 msReduced images only5,400 ms+ deferred widgets4,100 ms+ no speculative prefetch3,600 ms0 ms2,500 ms5,000 ms7,500 ms10,000 ms
A 50% improvement on a cohort that was never going to reach Good. That is still the right work: the cohort is small, and its experience was unusable.

What the segmentation is actually good for

Three uses justify carrying the field; a fourth is a trap worth naming.

Explaining variance you would otherwise chase. A p75 that moved without a deploy is frequently a traffic-mix change, and network mix is one of the two dimensions that moves it. Checking cohort volumes before investigating a regression saves days.

Justifying adaptive loading with evidence. “12% of our mobile sessions report 3g or worse and their p75 LCP is 6 s” is the sentence that gets adaptive loading prioritised. The abstract argument never does.

Setting realistic per-cohort expectations. The 2g cohort will not reach Good, and a target that pretends otherwise wastes everyone’s time. A target of “no cohort above 5 s” is achievable and still meaningful.

Not for personalising content. Using the network signal to decide what a user is shown, rather than how it is delivered, produces a product that behaves differently for people on worse connections in ways they did not choose and cannot see. Degrade delivery; keep the product the same.

Verifying it works

  1. The unknown share matches your Safari and Firefox share, roughly. A much larger unknown means the read is happening before the API is available or is throwing.
  2. 4g dominates. Typically 80–90% of Chromium traffic. If it does not, check that you are not reading a stale value late in the session.
  3. The cross-cut grid has a monotonic shape — worse as you move down and right. Non-monotonic cells usually mean too little volume, not an interesting finding.
  4. Policy is recorded on every beacon, and reduced-experience sessions can be excluded from like-for-like comparisons.
  5. Constrained-cohort metrics improve after adaptive loading ships, and the unconstrained cohort does not change.

Where the signal is strong enough to alert on

Most network cohorts are too small and too volatile to page anyone about, with one exception worth wiring up: a sudden change in the mix rather than in the values.

A constrained-cohort share that doubles overnight is nearly always one of three things — a marketing campaign reaching a new market, a CDN region failing over and degrading real throughput, or a release that broke the read and is now labelling everything unknown. All three are worth knowing about within the hour, and none of them shows up in a percentile.

-- Mix drift, not value drift: alert when the cohort composition moves.
SELECT
  network,
  countIf(ts >= now() - INTERVAL 1 HOUR) / countIf(ts >= now() - INTERVAL 1 HOUR AND 1)          AS share_now,
  countIf(ts <  now() - INTERVAL 1 HOUR) / greatest(countIf(ts < now() - INTERVAL 1 HOUR), 1)    AS share_base
FROM rum_events
WHERE metric = 'LCP' AND ts >= now() - INTERVAL 8 DAY
GROUP BY network;

The rule that works in practice: alert when any cohort’s share moves by more than half its baseline over an hour, with a minimum volume floor so quiet periods do not fire. The rest of the alerting design — persistence, sample floors, routing — is in Alerting & Regression Detection.

How the connection estimate is distributedA third of all sessions report nothing because Safari and Firefox expose no signal. The Save-Data row is a different population entirely — and the one adaptive loading exists for.How the connection estimate is distributedShare of sessions by reported value, mobile trafficChromium sessions86%9%All sessions58%6%34%Save-Data enabled41%32%27%4g3g2g or slowerunknown
A third of all sessions report nothing because Safari and Firefox expose no signal. The Save-Data row is a different population entirely — and the one adaptive loading exists for.

Edge cases and gotchas

  • It is a rolling estimate, not a measurement. It reflects recent observed throughput, which on a page that has loaded very little may be based on almost nothing.
  • It changes mid-session. Read once; a beacon labelled with a value read after the metric was captured is mislabelled.
  • Safari and Firefox expose nothing. On iOS-heavy traffic the majority of sessions are unknown, and no amount of care fixes that.
  • saveData is rare but meaningful. It is an explicit user choice and should override every other signal.
  • rtt and downlink are heavily rounded for fingerprinting resistance. Useful in aggregate; meaningless per session.
  • Do not gate critical functionality on it. A misread signal must never be the reason a user cannot complete a purchase.

FAQ

Is effectiveType reliable enough to change what I ship?

For delivery decisions — image quality, speculative prefetching, deferring non-essential widgets — yes, provided the degradation is graceful. For anything that changes what the user can do, no.

Why is almost all my traffic 4g?

Because 4g is an open-ended top bucket that includes wifi, fibre and 5G. It is not evidence that your users are on a mobile network, and it will not discriminate within that group.

Should I use rtt instead?

Only in aggregate. It is rounded to 25 ms increments deliberately, so per-session it carries little information, but the distribution across a cohort is informative.

How do I handle Safari, where nothing is exposed?

Label those sessions unknown and analyse them separately. Do not infer a connection from device or geography — you will be wrong in exactly the cases that matter.