Is navigator.connection a fingerprint in Firefox?

Short version: yes, but not in the way people expect. navigator.connection is useless as a value on Firefox, because the value does not exist. It is useful as a presence check. The Network Information API is a Chromium feature, so a genuine Firefox reports undefined for it, and any browser that claims to be Firefox while handing back an effectiveType or an rtt number has just told the page that its user agent is a costume.

This page is about that cross-check: not what navigator.connection reports, but whether the property being there at all agrees with the browser family the rest of the session claims to be.

What the Network Information API is

navigator.connection is the entry point to the Network Information API. On a browser that implements it, it exposes a NetworkInformation object with fields describing the current connection:

  • effectiveType - a coarse bucket like "4g" or "3g", estimated from recent throughput rather than the physical link.
  • downlink - an estimated downstream bandwidth in megabits per second.
  • rtt - an estimated round-trip time in milliseconds.
  • saveData - whether the user asked for reduced data usage.

It was designed so a page could adapt to a slow phone connection: serve smaller images, defer prefetching. That is the honest use. The dishonest use is fingerprinting, because those numbers vary between machines and networks and add a few more bits to a visitor hash. But all of that is a Chromium story. On Firefox the object is simply not present.

Why a real Firefox returns undefined

Firefox has never shipped the Network Information API to the open web. The property is absent, so reading it does not throw, it just yields undefined:

// In any real Firefox, at the console:
navigator.connection            // undefined
navigator.connection?.rtt       // undefined
'connection' in navigator       // false

That last line is the one detectors actually run. 'connection' in navigator is a one-token check that returns false on every real Firefox and true on every real Chromium. It costs nothing, cannot be argued with, and it is exactly the kind of member-existence probe that suites like CreepJS and BotD lean on to decide which engine you really are, regardless of what the user-agent string says.

So the failure mode is not “Firefox leaks connection data”. Firefox has no connection data to leak. The failure mode belongs to fake Firefox setups: a Chromium engine with a Firefox user agent painted over it, or a spoofing layer that forgot to delete a property that should never have been there. Both expose navigator.connection and both get caught by its mere presence.

Reading it with invisible_playwright

invisible_playwright is a real Firefox patched at the C++ level and driven by stock Playwright, so there is nothing to spoof here: the property is absent because the browser is genuinely Firefox. You can confirm it the same way a detector would.

from invisible_playwright import InvisiblePlaywright

with InvisiblePlaywright(seed=42) as browser:
    page = browser.new_page()
    page.goto("https://example.com")

    result = page.evaluate("""() => ({
        hasConnection: 'connection' in navigator,
        connection: navigator.connection,
        effectiveType: navigator.connection?.effectiveType ?? null,
        rtt: navigator.connection?.rtt ?? null,
    })""")

    print(result)
    # {'hasConnection': False, 'connection': None, 'effectiveType': None, 'rtt': None}

'connection' in navigator comes back False, and every field reads as absent, matching a stock Firefox on the same machine. The seed=42 is there only so the run is reproducible: the connection result does not depend on the seed, because there is no value to derive. Every other seed-driven surface (GPU, canvas, audio, fonts) is generated, but this particular member is generated by not existing, which is the correct Firefox behaviour.

If you want to see the contrast, run the same evaluate in a stock Chromium build. You will get hasConnection: True and a real effectiveType. That is the difference a UA-family cross-check is reading.

The cross-check that unmasks a fake Firefox

The single idea worth taking away: detectors do not trust the user-agent string, they test whether the members of the navigator object match the family that string claims. navigator.connection is one entry in a longer list of engine-specific properties, and the check is always the same shape.

Property Present on A Firefox UA showing it means
navigator.connection Chromium The engine is not really Firefox
navigator.getBattery Chromium (removed from Firefox) Same
navigator.userAgentData Chromium Same
chrome (the global object) Chromium Same

A browser that claims to be Firefox has to be missing all of these, consistently. Deleting them by hand from a Chromium engine is a losing game, because there is no public list of every member that differs and the set moves with each release. A single survivor is a contradiction, and a contradiction scores worse than an honest Chromium would have. This is the same trap as changing the user agent and nothing else: one field edited in isolation just disagrees with the forty around it.

Doing it the other way is the whole point of running a real Firefox. Because the engine is Firefox, the Chromium-only members are absent for the same reason they are absent in the browser on your desktop: they were never compiled in. There is no list to keep up to date, because nothing is being removed. See what data websites collect about your browser for how many of these small member-existence checks stack up into a single verdict.

What this does and does not do for you

Getting navigator.connection right is table stakes, not a finish line. It puts the JavaScript and driver layer in agreement with a real Firefox, which is why a session built this way passes most member-existence and fingerprint checks: navigator.webdriver reads as a real browser’s, the canvas and audio hashes are internally consistent, and the user agent matches the engine underneath it rather than claiming a browser the handshake contradicts.

What none of that touches:

  • IP reputation. A perfect browser on a flagged datacenter address still loses. The connection API says nothing about your exit, and neither does anything else in the page. You supply a clean proxy.
  • Per-account quotas and rate limits. effectiveType being absent does not raise a quota. Those are counted server-side against your account and your address, not read from the browser.
  • Behaviour and timing. Pointer motion, typing rhythm, and pacing are a separate layer that a member-existence check cannot see and this property cannot help with.

The honest framing: invisible_playwright makes the browser look like a real browser driven by a real person, which is necessary and not sufficient. It does not make you undetectable, and no browser property can. It removes the fingerprint and driver tells so that the remaining variables - your address and your behaviour - are the only ones left to get right.

Conclusion

navigator.connection is a fingerprint on Firefox mostly by its absence. There is no value to spoof, tune, or randomise: a real Firefox returns undefined, and the only way to fail this check is to expose a Chromium-only property while claiming to be Firefox. The defensible position is not a cleverer spoof but an engine that genuinely lacks the property, so the cross-check between user-agent family and available navigator members comes back consistent for the same reason it does on a real desktop. That closes the member-existence tell and leaves the parts no browser can fix - the IP and the behaviour

  • as your job.

Short answers to the questions that lead here

Does Firefox support navigator.connection? No. The Network Information API is Chromium-only, so navigator.connection is undefined and 'connection' in navigator is false in every real Firefox.

Should I spoof effectiveType and rtt to look like Firefox? No - the opposite. On Firefox those fields must not exist at all. Adding them is what gives away a fake Firefox.

How do detectors use this? As a family cross-check. They run 'connection' in navigator and compare the answer to the browser your user agent claims. A Firefox UA with the property present is a contradiction.

Is navigator.connection reliable for fingerprinting? Only on Chromium, and even there it is coarse. As a presence signal across engines it is very reliable, because the property either exists or it does not.

Does hiding it improve my IP reputation or quotas? No. It is a browser-property check with no bearing on your address, your rate limits, or your account. Those are separate problems you solve with a clean proxy and human pacing.

Do I need to configure anything in invisible_playwright for this? No. The engine is a real Firefox, so the property is absent out of the box, matching a stock Firefox on the same machine.

Sources

  • The Network Information API definition (effectiveType, downlink, rtt, saveData) and its Chromium-only implementation status, read from the standard and from browser support tables.
  • This project’s own fingerprint-parity checks, which compare the wrapper against a stock Firefox field by field on the same machine, including navigator member existence.

See also: is changing the user agent enough, what data websites collect about your browser, and why navigator.webdriver is not the whole story.


Written while maintaining invisible_playwright, a Firefox patched at the C++ level driven by stock Playwright. The property is absent because the browser is really Firefox, which is a great deal harder to argue with than a deleted one.


Back to top

MIT licensed. Every page here is written against the current source of the thing it describes, and several record something we got wrong first.

This site uses Just the Docs, a documentation theme for Jekyll.