# DuckDuckGo

> Since signalsVersion 10 the DuckDuckGo browsers have their own class, duckduckgo. Nothing is excluded; reads follow the engine underneath, WebKit on Apple devices and Chromium elsewhere.

Source: https://whiskerjs.com/browsers/duckduckgo/ (last updated 2026-10-26)

> **In plain words:**
>
> DuckDuckGo makes its own browser for Mac, Windows, iPhone and Android. Underneath it uses the engine each system offers: Apple’s WebKit on the Mac and iPhone, Chromium on Windows and Android. To a web page it looks much like Safari or Chrome, so WhiskerJS checks for DuckDuckGo first and reports its own class, `duckduckgo`. Nothing is left out on purpose here; everything else works as in the browser with the same engine.

## At a glance

- Browser class: `duckduckgo` since signalsVersion 10 (`navigator.duckduckgo` with an `isDuckDuckGo` method, or a `Ddg/` or `DuckDuckGo/` token in the user agent). Before, the class of its engine: `safari` on macOS and iOS, `chromium` or `chromium_webview` elsewhere.
- Excluded components: none. The `duckduckgo` row of the exclusion table is empty.
- Engine: WebKit on macOS and iOS (vendor `Apple Computer, Inc.`), Chromium on Windows and Android. Engine-dependent reads and checks follow it.
- On macOS and iOS, as in Safari: `device_memory`, `heap_limit`, `ua_client_hints`, `ua_versions` and `keyboard_layout` are `unavailable` / `no_api`; a CPU count of 4 or 8 is the bucket `4_8`; `audio_latency` is read.
- Lab runs: on one Mac, 10 of 11 manual demo runs kept one visitorId at signalsVersion 9; **not yet run at signalsVersion 10 or 11**. See [Lab runs](https://whiskerjs.com/browsers/duckduckgo/#lab-runs).

## How DuckDuckGo is recognised

The class test checks for DuckDuckGo right after Brave and before everything else, because on Apple devices DuckDuckGo would otherwise match the Safari step. It looks for two things, without calling or awaiting anything:

1. `navigator.duckduckgo` with an `isDuckDuckGo` method. DuckDuckGo’s open-source content-scope scripts define it.
2. The browser’s own token in the user agent: `Ddg/<version>` on macOS, `DuckDuckGo/<version>` on iOS and Android.

Stock Safari, Safari with Advanced Fingerprinting Protection and look-alikes (the `DuckDuckBot/` crawler, a bare `window.duckduckgo`, an object without the method) are not classed `duckduckgo`; the library’s tests cover each case. Like every class, it is a hint from values the client controls.

## What works

- On **macOS and iOS** (WebKit) WhiskerJS reads what it reads in [Safari](https://whiskerjs.com/browsers/safari-macos/): canvas, WebGL, offline audio, fonts, font preferences, Math, codecs, plugins, locale, screen, storage presence, permission states and the display preference queries, plus `audio_latency`. Whether [`apple_pay`](https://whiskerjs.com/signals/apple-pay/) and [`private_click`](https://whiskerjs.com/signals/private-click/) report a value or `none` depends on what the app turns on.
- On **Windows and Android** (Chromium) the Chromium-only components ([`device_memory`](https://whiskerjs.com/signals/device-memory/), [`heap_limit`](https://whiskerjs.com/signals/heap-limit/), [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/), [`ua_versions`](https://whiskerjs.com/signals/ua-versions/)) can be read, as in [Chrome](https://whiskerjs.com/browsers/chrome-desktop/).
- [`canvas`](https://whiskerjs.com/signals/canvas/) and [`webgl_image`](https://whiskerjs.com/signals/webgl-image/): since signalsVersion 10, when the first two renders differ, up to two more run within the component’s 1000 ms timeout, and the value is accepted once two in a row agree. Only a mismatch that persists is `noisy` / `render_mismatch`.
- [`content_blockers`](https://whiskerjs.com/signals/content-blockers/): DuckDuckGo hides ads by default and collapses the closest empty parent of a hidden ad, so the bait container can disappear. Since signalsVersion 10 that reports every bait name, and layout is judged from a plain control element. It is outside both ids.

## What is unavailable or blocked

On macOS and iOS the five Chromium-only components report `unavailable` / `no_api`, as in Safari: `device_memory`, `heap_limit`, `ua_client_hints`, `ua_versions` and `keyboard_layout`. A non-ok status counts the same in every run, so these do not make the id unstable. [`webgpu`](https://whiskerjs.com/signals/webgpu/) depends on the version and the system.

## What WhiskerJS excludes and why

Nothing. DuckDuckGo’s help page on its web tracking protections names no API it randomises or replaces with a fixed value, so there is no evidence for a row. If DuckDuckGo adds seeded noise anyway, the [known-answer probes](https://whiskerjs.com/signals/noise-profile/) turn `canvas`, `webgl_image` or `audio` into `noisy` / `seeded_noise` instead of letting the id change.

## Integrity flags to expect

- [`engine_mismatch`](https://whiskerjs.com/docs/integrity/#engine_mismatch) checks the engine behind the class, which for `duckduckgo` is decided from `navigator.vendor` and the user agent: WebKit behaviour on Apple devices, V8 behaviour elsewhere. A user-agent switcher can raise it.
- [`ua_ch_mismatch`](https://whiskerjs.com/docs/integrity/#ua_ch_mismatch) can only run where Client Hints exist (Windows and Android).
- [`native_tampering`](https://whiskerjs.com/docs/integrity/#native_tampering) and [`prototype_lie`](https://whiskerjs.com/docs/integrity/#prototype_lie) are designed to spot scripts that override functions. Whether DuckDuckGo’s own injected scripts trip them has not been measured.

## What the lab runs showed

On one Mac (macOS 26.5.1), manual demo runs in the lab mode, a check, not a benchmark:

- **signalsVersion 9**: 11 runs over 6 page loads. 10 runs had one visitorId; one run had a second, because one canvas render differed from the other (`canvas` `noisy` / `render_mismatch`), which changed the `gpu` part. In 7 of the 11 runs `content_blockers` was `unavailable` / `no_layout`. The class was `safari`, as signalsVersion 9 had no DuckDuckGo class. The visitorIds differed from those of Brave, Chrome and Firefox on the same Mac.
- These two findings led to the extra renders and the content-blocker fix of signalsVersion 10.
- **signalsVersion 10 and 11** (11 is the live demo’s version since 2026-10-26; it changes nothing for DuckDuckGo): not yet run. DuckDuckGo has no automation interface, so the automated local run left it out; a manual run is pending.

DuckDuckGo on Windows, iOS and Android has not been run at all.

## Testing it yourself

Open the [live demo](https://whiskerjs.com/demo/) in DuckDuckGo: `browser_class` should say `duckduckgo`, and the demo’s detected browser should say DuckDuckGo. Run it several times, close the window, and run again; the [lab mode](https://whiskerjs.com/demo/#lab) copies one line per run, so you can count how many visitorIds you see. Nothing is sent anywhere.

## Lab runs for DuckDuckGo on macOS

Aggregated outcomes from [the lab runs](https://whiskerjs.com/browsers/#lab-runs) (updated 2026-10-26): how many runs and how many different visitorIds. A check on one device, not a benchmark.

| signalsVersion | Method | Browser | Runs | visitorIds | Note |
| --- | --- | --- | --- | --- | --- |
| 9 | manual demo runs, 2026-10-26 | DuckDuckGo, macOS (classed safari at signalsVersion 9) | 11 | 2 | 6 page loads; one run had a noisy canvas render (render_mismatch) that changed the gpu part; content_blockers was not ok in 7 of 11 runs |

## Support table for DuckDuckGo on macOS

Expected status of every component in this browser at signalsVersion 11, from [the support matrix](https://whiskerjs.com/browsers/support-matrix/) (updated 2026-10-26). Basis "code" means expected from the library code and documentation, not yet measured; "lab" means seen in the early lab sample at signalsVersion 1; "run" means seen in every run of the automated local run at signalsVersion 10 on a Mac; "ci" means seen in the CI browser matrix at signalsVersion 10 on Linux (automated, headless).

| Component | Status | Basis | Note |
| --- | --- | --- | --- |
| [`apple_pay`](https://whiskerjs.com/signals/apple-pay/) | varies | code | Depends on whether the app offers ApplePaySession; none where it is missing. |
| [`audio`](https://whiskerjs.com/signals/audio/) | ok | code | No exclusion row for duckduckgo. |
| [`audio_latency`](https://whiskerjs.com/signals/audio-latency/) | ok | code | Read on the WebKit engine; the context is closed at once. |
| [`browser_class`](https://whiskerjs.com/signals/browser-class/) | ok | code | Class duckduckgo since signalsVersion 10 (navigator.duckduckgo with isDuckDuckGo, or a Ddg/ token in the user agent); signalsVersion 9 classed it safari. |
| [`canvas`](https://whiskerjs.com/signals/canvas/) | ok | code | Rendered again when the first two renders differ (since signalsVersion 10); only a persistent mismatch is noisy / render_mismatch. At signalsVersion 9 one of 11 manual runs had a noisy canvas render. |
| [`canvas_readback`](https://whiskerjs.com/signals/canvas-readback/) | ok | code | exact, unless an extension adds canvas noise (then altered). |
| [`color_depth`](https://whiskerjs.com/signals/color-depth/) | ok | code |   |
| [`color_gamut`](https://whiskerjs.com/signals/color-gamut/) | ok | code |   |
| [`content_blockers`](https://whiskerjs.com/signals/content-blockers/) | ok | code | DuckDuckGo hides ads by default, so expect bait names or none. At signalsVersion 9 it was unavailable / no_layout in 7 of 11 manual runs; fixed in signalsVersion 10, not yet rerun here. |
| [`cpu_architecture`](https://whiskerjs.com/signals/cpu-architecture/) | ok | code |   |
| [`device_memory`](https://whiskerjs.com/signals/device-memory/) | unavailable | code | WebKit does not offer navigator.deviceMemory. Seen unavailable / no_api in the manual runs at signalsVersion 9. |
| [`dynamic_range`](https://whiskerjs.com/signals/dynamic-range/) | ok | code |   |
| [`font_preferences`](https://whiskerjs.com/signals/font-preferences/) | ok | code |   |
| [`fonts`](https://whiskerjs.com/signals/fonts/) | ok | code |   |
| [`fonts_canvas`](https://whiskerjs.com/signals/fonts-canvas/) | ok | code |   |
| [`forced_colors`](https://whiskerjs.com/signals/forced-colors/) | ok | code |   |
| [`hardware_concurrency`](https://whiskerjs.com/signals/hardware-concurrency/) | ok | code | Read 16 times; a 4 or an 8 is reported as the bucket 4_8, because WebKit alternates between them for scripts on its known-fingerprinting list. |
| [`heap_limit`](https://whiskerjs.com/signals/heap-limit/) | unavailable | code | performance.memory is a Chromium API. Reports unavailable / no_api. Seen unavailable / no_api in the manual runs at signalsVersion 9. |
| [`intl_locale`](https://whiskerjs.com/signals/intl-locale/) | ok | code |   |
| [`keyboard_layout`](https://whiskerjs.com/signals/keyboard-layout/) | unavailable | code | No navigator.keyboard here: unavailable / no_api. Seen unavailable / no_api in the manual runs at signalsVersion 9. |
| [`languages`](https://whiskerjs.com/signals/languages/) | ok | code |   |
| [`math`](https://whiskerjs.com/signals/math/) | ok | code |   |
| [`media_codecs`](https://whiskerjs.com/signals/media-codecs/) | ok | code |   |
| [`noise_profile`](https://whiskerjs.com/signals/noise-profile/) | ok | code | exact unless something adds noise to the test drawings. |
| [`pdf_viewer`](https://whiskerjs.com/signals/pdf-viewer/) | ok | code |   |
| [`permissions`](https://whiskerjs.com/signals/permissions/) | ok | code |   |
| [`pixel_ratio`](https://whiskerjs.com/signals/pixel-ratio/) | ok | code |   |
| [`platform`](https://whiskerjs.com/signals/platform/) | ok | code |   |
| [`plugins`](https://whiskerjs.com/signals/plugins/) | ok | code |   |
| [`prefers_contrast`](https://whiskerjs.com/signals/prefers-contrast/) | ok | code |   |
| [`private_click`](https://whiskerjs.com/signals/private-click/) | varies | code | Depends on whether the app turns on Private Click Measurement; none where it is off. |
| [`reduced_motion`](https://whiskerjs.com/signals/reduced-motion/) | ok | code |   |
| [`screen_frame`](https://whiskerjs.com/signals/screen-frame/) | ok | code |   |
| [`screen_resolution`](https://whiskerjs.com/signals/screen-resolution/) | ok | code |   |
| [`speech_voices`](https://whiskerjs.com/signals/speech-voices/) | ok | code | Digest of the voice list, or none; some browsers fill the list late. |
| [`storage`](https://whiskerjs.com/signals/storage/) | ok | code |   |
| [`time_zone`](https://whiskerjs.com/signals/time-zone/) | ok | code |   |
| [`touch_support`](https://whiskerjs.com/signals/touch-support/) | ok | code |   |
| [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/) | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. Seen unavailable / no_api in the manual runs at signalsVersion 9. |
| [`ua_versions`](https://whiskerjs.com/signals/ua-versions/) | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. Seen unavailable / no_api in the manual runs at signalsVersion 9. |
| [`vendor`](https://whiskerjs.com/signals/vendor/) | ok | code |   |
| [`vendor_flavors`](https://whiskerjs.com/signals/vendor-flavors/) | ok | code |   |
| [`webgl_image`](https://whiskerjs.com/signals/webgl-image/) | ok | code | Rendered again when the first two renders differ (since signalsVersion 10). |
| [`webgl_params`](https://whiskerjs.com/signals/webgl-params/) | ok | code | Where WebGL 1 is available; unavailable / no_context without it. |
| [`webgl_renderer`](https://whiskerjs.com/signals/webgl-renderer/) | ok | code | Often a generic string such as Apple GPU. |
| [`webgl_vendor`](https://whiskerjs.com/signals/webgl-vendor/) | ok | code |   |
| [`webgpu`](https://whiskerjs.com/signals/webgpu/) | varies | code | Depends on browser version, operating system and GPU; needs a secure context. unavailable / no_api without navigator.gpu, no_context without an adapter. Seen ok in every run of the automated local run at signalsVersion 10 (default settings). |

## Questions and answers

### Why does DuckDuckGo need its own class?

On a Mac or an iPhone the DuckDuckGo browser looks like Safari to a page: the Apple vendor and a Safari-like user agent. Until signalsVersion 9 WhiskerJS classed it safari, so a server could not tell the two apart. Since signalsVersion 10 it is classed duckduckgo, recognised by navigator.duckduckgo with an isDuckDuckGo method or by the browser's own user-agent token.

### Does WhiskerJS exclude anything in DuckDuckGo?

No. DuckDuckGo's help page on its web tracking protections names no browser API it randomises, so the duckduckgo row of the exclusion table is empty. If a measurement shows otherwise, the row changes and signalsVersion goes up.

### Is the id stable in DuckDuckGo?

Not measured at signalsVersion 10 or 11 yet. At signalsVersion 9, on one Mac, 11 manual demo runs gave one visitorId 10 times; the odd run had one perturbed canvas render. signalsVersion 10 renders again when two renders differ, which is meant to settle such a one-off, but a manual rerun is still pending.

### Can the local real-browser script test DuckDuckGo?

No. DuckDuckGo offers no WebDriver or other automation interface, so the library's script lists it as manual. DuckDuckGo is tested by hand with the demo's lab mode.

## Related pages

- [browser_class](https://whiskerjs.com/signals/browser-class/): The class test, step by step.
- [Safari on macOS](https://whiskerjs.com/browsers/safari-macos/): The same engine on the Mac.
- [canvas](https://whiskerjs.com/signals/canvas/): The extra renders of signalsVersion 10.
- [content_blockers](https://whiskerjs.com/signals/content-blockers/): DuckDuckGo hides ads by default.
- [Previous ids](https://whiskerjs.com/docs/identifiers/#previous-ids-linking-across-a-version-bump): Linking the signalsVersion 9 id, when DuckDuckGo was classed by its engine.
- [Browser support matrix](https://whiskerjs.com/browsers/support-matrix/): All browsers side by side.
