[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #35 · 2026-10-11 · Example · Easy · for [geo testing](https://cdpfleet.com/geo-testing), [testing](https://cdpfleet.com/cross-browser-testing)

# A German IP with a UTC clock: making location, timezone and permissions agree

What a page learns about where you are — geolocation, its permission, timezone and language — through a German proxy, with defaults and with a Berlin context. Defaults contradict the IP; three context options fix it.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

Run on production on 2026-10-11: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

A proxy changes where your traffic comes from, not what the browser says about itself. Sites compare the IP's country with the browser's timezone, language and — if they ask — its geolocation. A German IP with a UTC clock, `en-US` and a refused location prompt is an easy inconsistency to flag. What does the browser report by default, and what does it take to make it agree with the proxy?

## What we used, and why

| What | Why |
| --- | --- |
| A German proxy | The exit the site sees; ip-api.com confirms its country and timezone. |
| `navigator.geolocation` and `navigator.permissions` | What a page reads when it asks where you are. |
| `Intl.DateTimeFormat().resolvedOptions().timeZone`, `navigator.language` | The clock and language the browser reports. |
| `newContext({ permissions: ["geolocation"], geolocation, timezoneId, locale })` | Per-context overrides for all of them — no relaunch. |

## How it works

1.  Launch Chromium through a German proxy and look up the exit country and timezone.
2.  In a default context, read geolocation, its permission, timezone and language.
3.  Repeat with the geolocation permission granted but no position set.
4.  Repeat with Berlin coordinates, `Europe/Berlin` and `de-DE`.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/35-geolocation-and-permissions), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`
-   `PROXY_URL_DE` — a proxy exit in Germany

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const BERLIN = { latitude: 52.52, longitude: 13.405 };

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL_DE, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

// What a page can learn about where the visitor is, from JavaScript.
const PROBE = `async () => {
  const geo = await new Promise((resolve) => navigator.geolocation.getCurrentPosition(
    (p) => resolve({ lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 }),
    (e) => resolve({ error: e.code === 1 ? 'permission denied' : e.message }),
    { timeout: 5000 }));
  const perm = (await navigator.permissions.query({ name: 'geolocation' })).state;
  return { geolocation: geo, permission: perm, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, utc_offset_min: -new Date().getTimezoneOffset() };
}`;

try {
  const out = { ip: null, contexts: [] };
  const setups = [
    ['defaults', {}],
    ['permission granted, no position', { permissions: ['geolocation'] }],
    ['Berlin: geolocation + timezone + locale', { permissions: ['geolocation'], geolocation: BERLIN, timezoneId: 'Europe/Berlin', locale: 'de-DE' }],
  ];
  for (const [label, options] of setups) {
    const ctx = await browser.newContext(options);
    const page = await ctx.newPage();
    if (!out.ip) {
      await page.goto('http://ip-api.com/json/?fields=query,countryCode,timezone', { timeout: 60000 });
      const ip = JSON.parse(await page.textContent('body'));
      out.ip = { country: ip.countryCode, timezone: ip.timezone };
    }
    await page.goto('https://httpbin.org/html', { timeout: 60000 });
    out.contexts.push({ setup: label, ...(await page.evaluate(`(${PROBE})()`)) });
    await ctx.close();
  }
  console.log(JSON.stringify(out, null, 2));
} finally {
  await browser.close();
}
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
import json
import os

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]
BERLIN = {"latitude": 52.52, "longitude": 13.405}

# What a page can learn about where the visitor is, from JavaScript.
PROBE = """async () => {
  const geo = await new Promise((resolve) => navigator.geolocation.getCurrentPosition(
    (p) => resolve({ lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 }),
    (e) => resolve({ error: e.code === 1 ? 'permission denied' : e.message }),
    { timeout: 5000 }));
  const perm = (await navigator.permissions.query({ name: 'geolocation' })).state;
  return { geolocation: geo, permission: perm, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, utc_offset_min: -new Date().getTimezoneOffset() };
}"""

res = requests.post("https://starter.cdpfleet.com/chromium/session",
    headers={"x-api-key": KEY},
    json={"proxy": os.environ["PROXY_URL_DE"], "headless": "new"}, timeout=60)
if not res.ok:
    raise SystemExit(f"launch {res.status_code} {res.text}")

with sync_playwright() as p:
    browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
    try:
        out = {"ip": None, "contexts": []}
        setups = [
            ("defaults", {}),
            ("permission granted, no position", {"permissions": ["geolocation"]}),
            ("Berlin: geolocation + timezone + locale", {"permissions": ["geolocation"], "geolocation": BERLIN, "timezone_id": "Europe/Berlin", "locale": "de-DE"}),
        ]
        for label, options in setups:
            ctx = browser.new_context(**options)
            page = ctx.new_page()
            if out["ip"] is None:
                page.goto("http://ip-api.com/json/?fields=query,countryCode,timezone", timeout=60000)
                ip = json.loads(page.text_content("body"))
                out["ip"] = {"country": ip["countryCode"], "timezone": ip["timezone"]}
            page.goto("https://httpbin.org/html", timeout=60000)
            out["contexts"].append({"setup": label, **page.evaluate(f"({PROBE})()")})
            ctx.close()
        print(json.dumps(out, indent=2, ensure_ascii=False))
    finally:
        browser.close()
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final Gson GSON = new GsonBuilder().serializeNulls().create();

  // What a page can learn about where the visitor is, from JavaScript.
  static final String PROBE = """
      async () => {
        const geo = await new Promise((resolve) => navigator.geolocation.getCurrentPosition(
          (p) => resolve({ lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 }),
          (e) => resolve({ error: e.code === 1 ? 'permission denied' : e.message }),
          { timeout: 5000 }));
        const perm = (await navigator.permissions.query({ name: 'geolocation' })).state;
        return { geolocation: geo, permission: perm, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, utc_offset_min: -new Date().getTimezoneOffset() };
      }""";

  public static void main(String[] args) throws Exception {
    String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL_DE")) + ", \"headless\": \"new\"}";
    HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        JsonObject out = new JsonObject();
        out.add("ip", JsonNull.INSTANCE);
        JsonArray contexts = new JsonArray();
        out.add("contexts", contexts);

        Map<String, Browser.NewContextOptions> setups = new LinkedHashMap<>();
        setups.put("defaults", new Browser.NewContextOptions());
        setups.put("permission granted, no position", new Browser.NewContextOptions().setPermissions(List.of("geolocation")));
        setups.put("Berlin: geolocation + timezone + locale", new Browser.NewContextOptions().setPermissions(List.of("geolocation"))
            .setGeolocation(52.52, 13.405).setTimezoneId("Europe/Berlin").setLocale("de-DE"));

        for (Map.Entry<String, Browser.NewContextOptions> setup : setups.entrySet()) {
          BrowserContext ctx = browser.newContext(setup.getValue());
          Page page = ctx.newPage();
          if (out.get("ip").isJsonNull()) {
            page.navigate("http://ip-api.com/json/?fields=query,countryCode,timezone", new Page.NavigateOptions().setTimeout(60000));
            JsonObject ip = JsonParser.parseString(page.textContent("body")).getAsJsonObject();
            JsonObject where = new JsonObject();
            where.add("country", ip.get("countryCode"));
            where.add("timezone", ip.get("timezone"));
            out.add("ip", where);
          }
          page.navigate("https://httpbin.org/html", new Page.NavigateOptions().setTimeout(60000));
          JsonObject entry = new JsonObject();
          entry.addProperty("setup", setup.getKey());
          for (Map.Entry<String, JsonElement> e : GSON.toJsonTree(page.evaluate("(" + PROBE + ")()")).getAsJsonObject().entrySet()) entry.add(e.getKey(), e.getValue());
          contexts.add(entry);
          ctx.close();
        }

        System.out.println(new GsonBuilder().serializeNulls().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
      } finally {
        browser.close();
      }
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

// What a page can learn about where the visitor is, from JavaScript.
const string Probe = """
    async () => {
      const geo = await new Promise((resolve) => navigator.geolocation.getCurrentPosition(
        (p) => resolve({ lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 }),
        (e) => resolve({ error: e.code === 1 ? 'permission denied' : e.message }),
        { timeout: 5000 }));
      const perm = (await navigator.permissions.query({ name: 'geolocation' })).state;
      return { geolocation: geo, permission: perm, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, utc_offset_min: -new Date().getTimezoneOffset() };
    }
    """;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
    new { proxy = Environment.GetEnvironmentVariable("PROXY_URL_DE"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
    JsonObject? ip = null;
    var contexts = new JsonArray();
    var setups = new (string Label, BrowserNewContextOptions Options)[]
    {
        ("defaults", new()),
        ("permission granted, no position", new() { Permissions = ["geolocation"] }),
        ("Berlin: geolocation + timezone + locale", new()
        {
            Permissions = ["geolocation"],
            Geolocation = new() { Latitude = 52.52f, Longitude = 13.405f },
            TimezoneId = "Europe/Berlin",
            Locale = "de-DE",
        }),
    };
    foreach (var (label, options) in setups)
    {
        var ctx = await browser.NewContextAsync(options);
        var page = await ctx.NewPageAsync();
        if (ip is null)
        {
            await page.GotoAsync("http://ip-api.com/json/?fields=query,countryCode,timezone", new() { Timeout = 60000 });
            var info = JsonNode.Parse((await page.TextContentAsync("body"))!)!;
            ip = new JsonObject { ["country"] = info["countryCode"]?.DeepClone(), ["timezone"] = info["timezone"]?.DeepClone() };
        }
        await page.GotoAsync("https://httpbin.org/html", new() { Timeout = 60000 });
        var entry = new JsonObject { ["setup"] = label };
        foreach (var prop in ((JsonObject)Plain(await page.EvaluateAsync<JsonElement>($"({Probe})()"))!).ToList())
            entry[prop.Key] = prop.Value?.DeepClone();
        contexts.Add(entry);
        await ctx.CloseAsync();
    }

    var result = new JsonObject { ["ip"] = ip, ["contexts"] = contexts };
    Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
    await browser.CloseAsync();
}

// EvaluateAsync<JsonElement> tags every object with a "$id" reference key; drop those.
static JsonNode? Plain(JsonElement e) => e.ValueKind switch
{
    JsonValueKind.Object => new JsonObject(e.EnumerateObject().Where(p => p.Name != "$id")
        .Select(p => KeyValuePair.Create(p.Name, Plain(p.Value)))),
    JsonValueKind.Array => new JsonArray(e.EnumerateArray().Select(Plain).ToArray()),
    _ => JsonNode.Parse(e.GetRawText()),
};
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

// What a page can learn about where the visitor is, from JavaScript.
const probe = `async () => {
  const geo = await new Promise((resolve) => navigator.geolocation.getCurrentPosition(
    (p) => resolve({ lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 }),
    (e) => resolve({ error: e.code === 1 ? 'permission denied' : e.message }),
    { timeout: 5000 }));
  const perm = (await navigator.permissions.query({ name: 'geolocation' })).state;
  return { geolocation: geo, permission: perm, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, utc_offset_min: -new Date().getTimezoneOffset() };
}`

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

type ipInfo struct {
	Country  string `json:"country"`
	Timezone string `json:"timezone"`
}

type contextResult struct {
	Setup        string `json:"setup"`
	Geolocation  any    `json:"geolocation"`
	Permission   any    `json:"permission"`
	Timezone     any    `json:"timezone"`
	Language     any    `json:"language"`
	UTCOffsetMin any    `json:"utc_offset_min"`
}

type output struct {
	IP       *ipInfo         `json:"ip"`
	Contexts []contextResult `json:"contexts"`
}

type setup struct {
	label   string
	options playwright.BrowserNewContextOptions
}

func run(browser playwright.Browser) (output, error) {
	out := output{Contexts: []contextResult{}}
	setups := []setup{
		{"defaults", playwright.BrowserNewContextOptions{}},
		{"permission granted, no position", playwright.BrowserNewContextOptions{Permissions: []string{"geolocation"}}},
		{"Berlin: geolocation + timezone + locale", playwright.BrowserNewContextOptions{
			Permissions: []string{"geolocation"},
			Geolocation: &playwright.Geolocation{Latitude: 52.52, Longitude: 13.405},
			TimezoneId:  playwright.String("Europe/Berlin"),
			Locale:      playwright.String("de-DE"),
		}},
	}
	for _, s := range setups {
		ctx, err := browser.NewContext(s.options)
		if err != nil {
			return out, err
		}
		page, err := ctx.NewPage()
		if err != nil {
			return out, err
		}
		if out.IP == nil {
			if _, err := page.Goto("http://ip-api.com/json/?fields=query,countryCode,timezone", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
				return out, err
			}
			text, err := page.TextContent("body")
			if err != nil {
				return out, err
			}
			var ip struct {
				CountryCode string `json:"countryCode"`
				Timezone    string `json:"timezone"`
			}
			if err := json.Unmarshal([]byte(text), &ip); err != nil {
				return out, err
			}
			out.IP = &ipInfo{Country: ip.CountryCode, Timezone: ip.Timezone}
		}
		if _, err := page.Goto("https://httpbin.org/html", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
			return out, err
		}
		v, err := page.Evaluate("(" + probe + ")()")
		if err != nil {
			return out, err
		}
		m := v.(map[string]any)
		out.Contexts = append(out.Contexts, contextResult{
			Setup:        s.label,
			Geolocation:  m["geolocation"],
			Permission:   m["permission"],
			Timezone:     m["timezone"],
			Language:     m["language"],
			UTCOffsetMin: m["utc_offset_min"],
		})
		if err := ctx.Close(); err != nil {
			return out, err
		}
	}
	return out, nil
}

func main() {
	session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL_DE"), "headless": "new"})
	if err != nil {
		log.Fatal(err)
	}
	pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()
	browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
	if err != nil {
		log.Fatal(err)
	}

	out, err := run(browser)
	browser.Close()
	if err != nil {
		log.Fatal(err)
	}
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "  ")
	enc.Encode(out)
	fmt.Print(buf.String())
}
```

## What we got

| Context | Geolocation | Permission | Timezone | Language |
| --- | --- | --- | --- | --- |
| defaults | permission denied | prompt | UTC | en-US |
| permission granted, no position | Timeout expired | granted | UTC | en-US |
| Berlin: geolocation + timezone + locale | 52.52, 13.41 | granted | Europe/Berlin | de-DE |

From the Node.js run on 2026-10-11. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
{
  "ip": {
    "country": "DE",
    "timezone": "Europe/Berlin"
  },
  "contexts": [
    {
      "setup": "defaults",
      "geolocation": {
        "error": "permission denied"
      },
      "permission": "prompt",
      "timezone": "UTC",
      "language": "en-US",
      "utc_offset_min": 0
    },
    {
      "setup": "permission granted, no position",
      "geolocation": {
        "error": "Timeout expired"
      },
      "permission": "granted",
      "timezone": "UTC",
      "language": "en-US",
      "utc_offset_min": 0
    },
    {
      "setup": "Berlin: geolocation + timezone + locale",
      "geolocation": {
        "lat": 52.52,
        "lon": 13.41
      },
      "permission": "granted",
      "timezone": "Europe/Berlin",
      "language": "de-DE",
      "utc_offset_min": 120
    }
  ]
}
```

## Takeaways

-   **Defaults contradict the proxy:** the exit was German (`Europe/Berlin`) but the browser said UTC and `en-US`, and refused geolocation.
-   **Granting the permission isn't enough:** without a position, `getCurrentPosition` just times out — the page sees a permission it can't use.
-   **Three options make it consistent:** `geolocation`, `timezoneId` and `locale` on the context, plus the `geolocation` permission — the page then sees Berlin, `Europe/Berlin` (UTC+2) and `de-DE`.
-   **Match them to the exit, per context:** look up your proxy's country first (or use a country-pinned [residential token](https://cdpfleet.com/docs/proxies#residential)); Camoufox can do this for you with `geoip: true`.

---
Source: https://cdpfleet.com/docs/cases/geolocation-and-permissions · Markdown: https://cdpfleet.com/docs/cases/geolocation-and-permissions.md · All docs for AI tools: https://cdpfleet.com/llms.txt
