[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #3 · 2026-09-30 · Example · Easy · for [testing](https://cdpfleet.com/cross-browser-testing)

# Stable, beta, dev and an older major: four Chromes side by side

Pick a channel or pin a previous major, read the live catalog instead of hard-coding versions, and see what actually differs on the wire.

[Google Chrome](https://cdpfleet.com/docs/browsers/chrome)

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

## The problem

A site breaks on the new Chrome, or you need to test the next release before your users get it, or a target only trusts the current version. cdpfleet runs Chrome stable, beta and dev, plus previous majors. Which options select which build — and does the version change the fingerprint a site sees?

## What we used, and why

| What | Why |
| --- | --- |
| `GET /api/public/browsers` | The live catalog: which channels and previous majors exist right now. Majors rotate, so read it instead of hard-coding numbers. |
| `channel: "beta"` / `"dev"` | Selects a pre-release build. Omit it (or send `"stable"`) for stable. |
| `version: "154"` | Pins a retained previous major (154 and 153 on 2026-10-21). A major the fleet doesn't have returns `503 version_unavailable` instead of silently running another. Don't combine it with a pre-release channel (that's a `400`). |
| `headless: false` | A real (virtual) display, so the user agent reads `Chrome/…`, not `HeadlessChrome/…`. |

## How it works

1.  Read the catalog and build one variant per listed Chrome build.
2.  Launch all variants at once — each is its own session.
3.  In each, compare `browser.version()` with the catalog, and read the user agent, `sec-ch-ua` and fingerprints from tls.peet.ws.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/03-chrome-versions), 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`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// The live catalog says which channels and previous majors exist right now.
const catalog = await (await fetch('https://cdpfleet.com/api/public/browsers')).json();
const versions = catalog.engines.find((e) => e.key === 'chrome').versions;
const variants = versions.map((v) => (v.label === 'pinned'
  ? { label: `version ${v.version.split('.')[0]}`, options: { version: v.version.split('.')[0] }, catalog: v.version }
  : { label: `channel ${v.label}`, options: v.label === 'stable' ? {} : { channel: v.label }, catalog: v.version }));

async function probe({ label, options, catalog: expected }) {
  const res = await fetch('https://starter.cdpfleet.com/chrome/session', {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
    // headless: false (a real display) so the user agent doesn't say "HeadlessChrome".
    body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: false, ...options }),
  });
  if (!res.ok) return { variant: label, error: `${res.status} ${await res.text()}` };
  const { wsUrl } = await res.json();
  const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    // A residential exit occasionally times out: one retry, in a fresh tab.
    const load = async () => (await browser.newPage()).goto('https://tls.peet.ws/api/all', { timeout: 30000 });
    const fp = await (await load().catch(load)).json();
    const headers = fp.http2.sent_frames.find((f) => f.frame_type === 'HEADERS').headers;
    return {
      variant: label,
      catalog_version: expected,
      browser_version: browser.version(),
      user_agent: fp.user_agent,
      sec_ch_ua: headers.find((h) => h.startsWith('sec-ch-ua: '))?.slice(11) ?? null,
      ja4: fp.tls.ja4,
      akamai_h2_hash: fp.http2.akamai_fingerprint_hash,
    };
  } finally {
    await browser.close();
  }
}

// All variants at once: each is its own session.
console.log(JSON.stringify(await Promise.all(variants.map(probe)), null, 2));
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
from concurrent.futures import ThreadPoolExecutor

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]

# The live catalog says which channels and previous majors exist right now.
catalog = requests.get("https://cdpfleet.com/api/public/browsers", timeout=30).json()
versions = next(e for e in catalog["engines"] if e["key"] == "chrome")["versions"]
variants = []
for v in versions:
    if v["label"] == "pinned":
        major = v["version"].split(".")[0]
        variants.append((f"version {major}", {"version": major}, v["version"]))
    else:
        variants.append((f"channel {v['label']}", {} if v["label"] == "stable" else {"channel": v["label"]}, v["version"]))

def probe(variant):
    label, options, expected = variant
    # headless: False (a real display) so the user agent doesn't say "HeadlessChrome".
    res = requests.post("https://starter.cdpfleet.com/chrome/session", headers={"x-api-key": KEY},
                        json={"proxy": os.environ["PROXY_URL"], "headless": False, **options}, timeout=60)
    if not res.ok:
        return {"variant": label, "error": f"{res.status_code} {res.text}"}
    # The sync API is per thread: each probe gets its own Playwright instance.
    with sync_playwright() as p:
        browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
        try:
            try:
                fp = browser.new_page().goto("https://tls.peet.ws/api/all", timeout=30000).json()
            except Exception:  # a residential exit occasionally times out: one retry, in a fresh tab
                fp = browser.new_page().goto("https://tls.peet.ws/api/all", timeout=30000).json()
            headers = next(f for f in fp["http2"]["sent_frames"] if f["frame_type"] == "HEADERS")["headers"]
            return {
                "variant": label,
                "catalog_version": expected,
                "browser_version": browser.version,
                "user_agent": fp["user_agent"],
                "sec_ch_ua": next((h[11:] for h in headers if h.startswith("sec-ch-ua: ")), None),
                "ja4": fp["tls"]["ja4"],
                "akamai_h2_hash": fp["http2"]["akamai_fingerprint_hash"],
            }
        finally:
            browser.close()

# All variants at once: each is its own session.
with ThreadPoolExecutor(len(variants)) as pool:
    print(json.dumps(list(pool.map(probe, variants)), indent=2))
```

```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
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final HttpClient HTTP = HttpClient.newHttpClient();

  static JsonObject probe(String label, JsonObject options, String expected) throws Exception {
    // headless: false (a real display) so the user agent doesn't say "HeadlessChrome".
    options.addProperty("proxy", System.getenv("PROXY_URL"));
    options.addProperty("headless", false);
    HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chrome/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(options.toString())).build(), HttpResponse.BodyHandlers.ofString());
    JsonObject out = new JsonObject();
    out.addProperty("variant", label);
    if (res.statusCode() != 200) { out.addProperty("error", res.statusCode() + " " + res.body()); return out; }
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
    // Playwright objects are per thread: each probe gets its own instance.
    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        Response nav;
        try {
          nav = browser.newPage().navigate("https://tls.peet.ws/api/all", new Page.NavigateOptions().setTimeout(30000));
        } catch (PlaywrightException err) { // a residential exit occasionally times out: one retry, in a fresh tab
          nav = browser.newPage().navigate("https://tls.peet.ws/api/all", new Page.NavigateOptions().setTimeout(30000));
        }
        JsonObject fp = JsonParser.parseString(nav.text()).getAsJsonObject();
        String secChUa = null;
        for (JsonElement f : fp.getAsJsonObject("http2").getAsJsonArray("sent_frames")) {
          if (!f.getAsJsonObject().get("frame_type").getAsString().equals("HEADERS")) continue;
          for (JsonElement h : f.getAsJsonObject().getAsJsonArray("headers")) {
            if (h.getAsString().startsWith("sec-ch-ua: ")) secChUa = h.getAsString().substring(11);
          }
        }
        out.addProperty("catalog_version", expected);
        out.addProperty("browser_version", browser.version());
        out.add("user_agent", fp.get("user_agent"));
        out.addProperty("sec_ch_ua", secChUa);
        out.add("ja4", fp.getAsJsonObject("tls").get("ja4"));
        out.add("akamai_h2_hash", fp.getAsJsonObject("http2").get("akamai_fingerprint_hash"));
        return out;
      } finally {
        browser.close();
      }
    }
  }

  public static void main(String[] args) throws Exception {
    // The live catalog says which channels and previous majors exist right now.
    JsonObject catalog = JsonParser.parseString(HTTP.send(HttpRequest.newBuilder(URI.create("https://cdpfleet.com/api/public/browsers")).build(),
        HttpResponse.BodyHandlers.ofString()).body()).getAsJsonObject();
    List<Callable<JsonObject>> jobs = new ArrayList<>();
    for (JsonElement e : catalog.getAsJsonArray("engines")) {
      if (!e.getAsJsonObject().get("key").getAsString().equals("chrome")) continue;
      for (JsonElement ve : e.getAsJsonObject().getAsJsonArray("versions")) {
        String labelName = ve.getAsJsonObject().get("label").getAsString();
        String version = ve.getAsJsonObject().get("version").getAsString();
        JsonObject options = new JsonObject();
        String label;
        if (labelName.equals("pinned")) {
          String major = version.split("\\.")[0];
          options.addProperty("version", major);
          label = "version " + major;
        } else {
          if (!labelName.equals("stable")) options.addProperty("channel", labelName);
          label = "channel " + labelName;
        }
        jobs.add(() -> probe(label, options, version));
      }
    }
    // All variants at once: each is its own session.
    ExecutorService pool = Executors.newFixedThreadPool(jobs.size());
    JsonArray rows = new JsonArray();
    for (Future<JsonObject> f : pool.invokeAll(jobs)) rows.add(f.get());
    pool.shutdown();
    System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(rows));
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var proxy = Environment.GetEnvironmentVariable("PROXY_URL")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();

// The live catalog says which channels and previous majors exist right now.
var catalog = JsonNode.Parse(await http.GetStringAsync("https://cdpfleet.com/api/public/browsers"))!;
var versions = catalog["engines"]!.AsArray().First(e => (string?)e!["key"] == "chrome")!["versions"]!.AsArray();
var variants = versions.Select(v =>
{
    var label = (string)v!["label"]!;
    var version = (string)v["version"]!;
    var options = new JsonObject { ["proxy"] = proxy, ["headless"] = false }; // a real display: no "HeadlessChrome"
    if (label == "pinned") { options["version"] = version.Split('.')[0]; return ($"version {version.Split('.')[0]}", options, version); }
    if (label != "stable") options["channel"] = label;
    return ($"channel {label}", options, version);
}).ToList();

async Task<JsonObject> Probe((string Label, JsonObject Options, string Expected) v)
{
    var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chrome/session", v.Options);
    if (!res.IsSuccessStatusCode) return new JsonObject { ["variant"] = v.Label, ["error"] = $"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}" };
    var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
    var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
    try
    {
        IResponse? nav;
        try { nav = await (await browser.NewPageAsync()).GotoAsync("https://tls.peet.ws/api/all", new() { Timeout = 30000 }); }
        catch (Exception err) when (err is PlaywrightException or TimeoutException) // a residential exit occasionally times out: one retry, in a fresh tab
        { nav = await (await browser.NewPageAsync()).GotoAsync("https://tls.peet.ws/api/all", new() { Timeout = 30000 }); }
        var fp = JsonNode.Parse(await nav!.TextAsync())!;
        var headers = fp["http2"]!["sent_frames"]!.AsArray().First(f => (string?)f!["frame_type"] == "HEADERS")!["headers"]!.AsArray();
        return new JsonObject
        {
            ["variant"] = v.Label,
            ["catalog_version"] = v.Expected,
            ["browser_version"] = browser.Version,
            ["user_agent"] = fp["user_agent"]!.DeepClone(),
            ["sec_ch_ua"] = headers.Select(h => (string)h!).FirstOrDefault(h => h.StartsWith("sec-ch-ua: "))?[11..],
            ["ja4"] = fp["tls"]!["ja4"]!.DeepClone(),
            ["akamai_h2_hash"] = fp["http2"]!["akamai_fingerprint_hash"]!.DeepClone(),
        };
    }
    finally
    {
        await browser.CloseAsync();
    }
}

// All variants at once: each is its own session.
var rows = new JsonArray((await Task.WhenAll(variants.Select(Probe))).ToArray<JsonNode?>());
Console.WriteLine(rows.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
```

```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
package main

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

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

var key = os.Getenv("CDPFLEET_API_KEY")

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 result struct {
	Variant        string  `json:"variant"`
	CatalogVersion string  `json:"catalog_version,omitempty"`
	BrowserVersion string  `json:"browser_version,omitempty"`
	UserAgent      string  `json:"user_agent,omitempty"`
	SecChUA        *string `json:"sec_ch_ua,omitempty"`
	JA4            string  `json:"ja4,omitempty"`
	AkamaiH2Hash   string  `json:"akamai_h2_hash,omitempty"`
	Error          string  `json:"error,omitempty"`
}

type peet struct {
	UserAgent string `json:"user_agent"`
	TLS       struct {
		JA4 string `json:"ja4"`
	} `json:"tls"`
	HTTP2 struct {
		AkamaiFingerprintHash string `json:"akamai_fingerprint_hash"`
		SentFrames            []struct {
			FrameType string   `json:"frame_type"`
			Headers   []string `json:"headers"`
		} `json:"sent_frames"`
	} `json:"http2"`
}

func probe(pw *playwright.Playwright, label string, options map[string]any, expected string) result {
	// headless: false (a real display) so the user agent doesn't say "HeadlessChrome".
	options["proxy"], options["headless"] = os.Getenv("PROXY_URL"), false
	session, err := launch("chrome", options)
	if err != nil {
		return result{Variant: label, Error: err.Error()}
	}
	browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
	if err != nil {
		return result{Variant: label, Error: err.Error()}
	}
	defer browser.Close()
	page, _ := browser.NewPage()
	res, err := page.Goto("https://tls.peet.ws/api/all", playwright.PageGotoOptions{Timeout: playwright.Float(30000)})
	if err != nil { // a residential exit occasionally times out: one retry, in a fresh tab
		page, _ = browser.NewPage()
		res, err = page.Goto("https://tls.peet.ws/api/all", playwright.PageGotoOptions{Timeout: playwright.Float(30000)})
	}
	if err != nil {
		return result{Variant: label, Error: err.Error()}
	}
	var fp peet
	res.JSON(&fp)
	r := result{Variant: label, CatalogVersion: expected, BrowserVersion: browser.Version(), UserAgent: fp.UserAgent,
		JA4: fp.TLS.JA4, AkamaiH2Hash: fp.HTTP2.AkamaiFingerprintHash}
	for _, f := range fp.HTTP2.SentFrames {
		for _, h := range f.Headers {
			if f.FrameType == "HEADERS" && strings.HasPrefix(h, "sec-ch-ua: ") {
				v := h[11:]
				r.SecChUA = &v
			}
		}
	}
	return r
}

func main() {
	// The live catalog says which channels and previous majors exist right now.
	res, err := http.Get("https://cdpfleet.com/api/public/browsers")
	if err != nil {
		log.Fatal(err)
	}
	var catalog struct {
		Engines []struct {
			Key      string                            `json:"key"`
			Versions []struct{ Label, Version string } `json:"versions"`
		} `json:"engines"`
	}
	json.NewDecoder(res.Body).Decode(&catalog)
	res.Body.Close()

	pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()

	var jobs []func() result
	for _, e := range catalog.Engines {
		if e.Key != "chrome" {
			continue
		}
		for _, v := range e.Versions {
			v := v
			label, options := "channel "+v.Label, map[string]any{}
			if v.Label == "pinned" {
				major := strings.Split(v.Version, ".")[0]
				label, options["version"] = "version "+major, major
			} else if v.Label != "stable" {
				options["channel"] = v.Label
			}
			jobs = append(jobs, func() result { return probe(pw, label, options, v.Version) })
		}
	}
	// All variants at once: each is its own session.
	results := make([]result, len(jobs))
	var wg sync.WaitGroup
	for i, job := range jobs {
		wg.Add(1)
		go func(i int, job func() result) { defer wg.Done(); results[i] = job() }(i, job)
	}
	wg.Wait()
	out, _ := json.MarshalIndent(results, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| Variant | browser.version() | User agent | sec-ch-ua | JA4 |
| --- | --- | --- | --- | --- |
| channel stable | 156.0.8078.79 | Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 | "Not:A-Brand";v="8", "Chromium";v="156", "Google Chrome";v="156" | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |
| channel beta | 157.0.8103.15 | Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/157.0.0.0 Safari/537.36 | "Not-A.Brand";v="99", "Google Chrome";v="157", "Chromium";v="157" | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |
| channel dev | 158.0.8106.0 | Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/158.0.0.0 Safari/537.36 | "Chromium";v="158", "Not.A/Brand";v="24", "Google Chrome";v="158" | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |
| version 155 | 155.0.8059.84 | Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 | "Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24" | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |
| version 154 | 154.0.8037.97 | Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 | "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99" | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |

From the Node.js run on 2026-10-30. 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)

```
[
  {
    "variant": "channel stable",
    "catalog_version": "156.0.8078.79",
    "browser_version": "156.0.8078.79",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
    "sec_ch_ua": "\"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"Google Chrome\";v=\"156\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  {
    "variant": "channel beta",
    "catalog_version": "158.0.8127.2",
    "browser_version": "157.0.8103.15",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/157.0.0.0 Safari/537.36",
    "sec_ch_ua": "\"Not-A.Brand\";v=\"99\", \"Google Chrome\";v=\"157\", \"Chromium\";v=\"157\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  {
    "variant": "channel dev",
    "catalog_version": "158.0.8117.2",
    "browser_version": "158.0.8106.0",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/158.0.0.0 Safari/537.36",
    "sec_ch_ua": "\"Chromium\";v=\"158\", \"Not.A/Brand\";v=\"24\", \"Google Chrome\";v=\"158\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  {
    "variant": "version 155",
    "catalog_version": "155.0.8059.84",
    "browser_version": "155.0.8059.84",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
    "sec_ch_ua": "\"Google Chrome\";v=\"155\", \"Chromium\";v=\"155\", \"Not(A:Brand\";v=\"24\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  {
    "variant": "version 154",
    "catalog_version": "154.0.8037.97",
    "browser_version": "154.0.8037.97",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36",
    "sec_ch_ua": "\"Chromium\";v=\"154\", \"Google Chrome\";v=\"154\", \"Not A(Brand\";v=\"99\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  }
]
```

## Takeaways

-   **Stable and pinned versions got the major the catalog lists.** The exact build usually matches to the last digit; while a new build rolls across the servers it can lag for a while (on 2026-10-30 stable reported 156.0.8078.47 on some servers where the catalog listed .79), since the catalog shows the most common build. **A new beta major rolls out the same way:** that day the catalog listed beta 158.0.8127.2 while some servers still served 157.0.8103.15, so `channel: "beta"` gave 157 or 158 depending on the server. **A pinned major gives you that major;** the patch level can differ between servers.
-   **The user agent only carries the major** (`Chrome/155.0.0.0`): Chrome's user-agent reduction. The full version is in `sec-ch-ua-full-version-list` for sites that ask.
-   **`sec-ch-ua` changes per major** (channels on the same major send the same header: beta and dev were both 158 on 2026-10-30) — including its "GREASE" brand (`Not_A Brand`, `Not(A:Brand`, `Not:A-Brand`…), which Chrome rotates on purpose. Copying one version's header onto another is detectable.
-   **TLS (JA4) and HTTP/2 fingerprints are identical from 153 to 158** (2026-10-21; 152 to 157 on 2026-10-04) — so a pinned major doesn't cost you anything on the network side.

---
Source: https://cdpfleet.com/docs/cases/chrome-versions · Markdown: https://cdpfleet.com/docs/cases/chrome-versions.md · All docs for AI tools: https://cdpfleet.com/llms.txt
