> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brightdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 了解您的 Browser API 带宽用量

> 查看 Browser API 带宽的用途，读懂带宽明细，并将您自己的 CDP 测量结果与计费数据进行核对。

本页介绍 Browser API 如何计量和计费带宽、带宽明细中每个字段的含义，以及为什么您在自己脚本中测量的数值可能与计费总量不同。

## 概述

您只为一个数值付费：`total`。它是您的会话经过代理隧道的所有字节，在网络线路上计量。带宽明细中的其他字段都用于解释 `total` 的构成，方便您与自己的测量结果进行核对。

如果您使用 Chrome DevTools Protocol（CDP）的 `Network.*` 事件自行统计流量，得到的数值会与 `total` 不同，并且通常更低。这是正常的。差异来自以下三个方面：

* **会话开始前完成的工作。** 对于部分网站，Browser API 会在您的目标页面加载前预热浏览器，例如先加载网站首页。这是我们以高成功率解锁网站的方式之一。预热发生在您的会话开始之前，因此您的脚本无法在 CDP 事件中看到这部分流量。它记录在 `before_session` 中。
* **协议开销。** 加密（TLS）握手和分帧会为每个连接增加字节。它们记录在 `overhead_bytes` 中。
* **Chrome 未向您的脚本报告的流量。** 部分请求经过网络，但不会产生您的脚本所监听的 CDP 事件。它们记录在 `client_hidden_bytes` 中。

计入以上三项后，通常仍会剩余少量差异（残差）。残差的几种可能原因请参阅[为什么您测量的数值与我们的计费不同](#为什么您测量的数值与我们的计费不同)。

> [!WARNING]
>
> **计费方式没有任何变化。** 带宽明细仅用于提升可见性和透明度。带宽的计量和计费方式与之前相同。

## 在哪里查看带宽明细

同一个明细对象可在两个位置获取，只有字段名称不同。

| 位置 | 计费总量 | 明细对象 |
| - | - | - |
| Session Logs API | `bandwidth`（数值，保持不变） | `bandwidth_breakdown` |
| CDP 命令 `Browser.getBandwidth` | 不单独返回，请使用 `bandwidth.total` | `bandwidth` |

**1. 会话结束后，通过 Session Logs API 获取。** 每条会话记录保留原有的 `bandwidth` 字段，即以数值表示的计费总量。明细位于新字段 `bandwidth_breakdown` 中。`bandwidth` 始终等于 `bandwidth_breakdown.total`。

```bash theme={null}
curl "https://api.brightdata.com/browser_sessions/{session_id}" \
  -H "Authorization: Bearer $API_KEY"
```

```json theme={null}
{
  "session": {
    "session_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "api_name": "scraping_browser1",
    "status": "finished",
    "target_url": "https://www.example.com",
    "bandwidth": 9685211,
    "bandwidth_breakdown": {
      "total": 9685211,
      "payload_bytes": 9667573,
      "overhead_bytes": 17638,
      "...": "..."
    },
    "error": null
  }
}
```

`GET /browser_sessions` 会为列表中的每个会话返回相同的两个字段。

**2. 在会话内部，通过 CDP 命令获取。** `Browser.getBandwidth` 仅返回明细对象，键名为 `bandwidth`。其字段与 API 中的 `bandwidth_breakdown` 相同。没有单独的数值字段，计费总量为 `bandwidth.total`。您可以随时调用该命令读取截至当前的带宽用量，例如在关闭浏览器之前调用。

```js theme={null}
const client = await page.createCDPSession();
const { bandwidth } = await client.send('Browser.getBandwidth');
console.log(bandwidth.total);
```

该命令的用法与其他 Browser API 自定义 CDP 命令（例如 `Browser.getSessionId`）相同。在会话中途读取的数值涵盖截至该时刻的流量，最终数值以 Session Logs API 为准。

## 带宽明细对象包含哪些字段

该对象在 Session Logs API 中名为 `bandwidth_breakdown`，在 CDP 命令中名为 `bandwidth`。对象包含两组字段。**计费字段**描述您付费的内容。**核对字段**按照 Chrome 的视角描述流量，方便您与自己的 CDP 数值进行核对。核对字段永远不会在 `total` 之外额外计费。

所有数值的单位均为字节。

```json theme={null}
{
  "total": 9685211,
  "payload_bytes": 9667573,
  "overhead_bytes": 17638,
  "session_bytes": 2973,
  "client_hidden_bytes": 14201,
  "before_session": 9567573,
  "session_breakdown": {
    "document": 2520, "script": 453, "stylesheet": 0, "image": 0,
    "font": 0, "media": 0, "other": 0
  },
  "before_session_breakdown": {
    "document": 412000, "script": 3105573, "stylesheet": 150000, "image": 5600000,
    "font": 300000, "media": 0, "other": 0
  }
}
```

下表说明每个字段。

### 计费字段

| 字段 | 是否计费 | 说明 |
| - | - | - |
| `total` | **是。这是唯一计费的数值。** | 您的会话通过代理隧道发送和接收的所有加密字节，在网络套接字处计量。 |
| `payload_bytes` | 属于 `total` 的一部分 | 请求和响应的加密内容（TLS 应用数据）。已经过压缩，与网络传输时的形态一致。 |
| `overhead_bytes` | 属于 `total` 的一部分 | `total` 减去 `payload_bytes`。包括 TLS 握手、每条加密记录的小型头部以及其他协议消息。不会为负数。 |

`total` 始终精确等于 `payload_bytes + overhead_bytes`。

### 核对字段

| 字段 | 是否计费 | 说明 |
| - | - | - |
| `session_bytes` | 否，不在 `total` 之外计费 | 会话期间的流量，根据 Chrome 的 CDP `Network.*` 事件统计。这是用于与您自己的 CDP 总和进行比较的数值。 |
| `client_hidden_bytes` | 否，不在 `total` 之外计费 | 会话期间 Chrome 接收到、但从未通过 `Network.requestWillBeSent` 通告的请求的字节数。您的脚本无法将其视为请求，但它们是真实的网络流量。通常为 `0`。 |
| `before_session` | 否，不在 `total` 之外计费 | 在您的会话开始前用于准备浏览器的流量（例如访问首页），属于解锁网站的一部分。统计方式与 `session_bytes` 相同。未运行预热时为 `0`。 |
| `session_breakdown` | 否 | 按资源类型拆分的 `session_bytes`。 |
| `before_session_breakdown` | 否 | 按资源类型拆分的 `before_session`。 |

两个明细中的资源类型：`document`、`script`、`stylesheet`、`image`、`font`、`media`、`other`。

`other` 包括所有没有独立字段的资源类型：API 调用（XHR 和 Fetch）、下载、WebSocket 和 EventSource 连接、Web 应用清单、文本轨道、预取和预检请求、ping、CSP 违规报告以及签名交换。它还包括 Chrome 报告为 "Other" 或没有类型的资源，以及 Chrome 未来新增的任何类型。

### `session_bytes` 如何统计

`session_bytes` 采用以下定义，与您通过标准 CDP 事件得到的结果一致：

* **请求：** 请求行、请求头以及请求体（POST 数据）。
* **响应：** 状态行、响应头以及传输时的响应体（`encodedDataLength`）。gzip 或 Brotli 响应按压缩后的大小计算。
* **请求头和响应头按未压缩形式计算**，即纯文本的 `Name: value`。在网络上，HTTP/2 会压缩头部，因此这部分会略大于线路上的字节数。

## 为什么您测量的数值与我们的计费不同

您的脚本和我们的计量器在两个不同的位置统计流量。您的脚本通过 Chrome 内部的 CDP 事件获知流量。我们的计量器在 Chrome 处理完成后，统计浏览器与代理之间网络连接上的加密字节。有些流量只存在于第二个位置，有些流量在两个位置的统计方式也不同。

```mermaid theme={null}
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Inter, ui-sans-serif, system-ui, sans-serif", "fontSize": "15px", "lineColor": "#818CF8", "clusterBkg": "#FAFAFF", "clusterBorder": "#C4B5FD", "titleColor": "#4C1D95", "edgeLabelBackground": "#FFFFFF"}, "flowchart": {"curve": "basis", "nodeSpacing": 24, "rankSpacing": 80, "padding": 18, "subGraphTitleMargin": {"top": 8, "bottom": 14}}}}%%
flowchart LR
  subgraph Chrome["Chrome 内部（Browser API）"]
    direction TB
    A("您会话中的页面请求<br/><code>session_bytes</code>"):::bwSeen
    B("会话开始前的预热<br/><code>before_session</code>"):::bwUnseen
    C("Chrome 未通告的请求<br/><code>client_hidden_bytes</code>"):::bwUnseen
    D("少量剩余差异（残差）<br/><code>不属于任何字段</code>"):::bwUnseen
  end
  S("您的脚本<br/>CDP Network.* 事件"):::bwScript
  M("代理隧道计量<br/><b>计费：total</b><br/>payload_bytes + overhead_bytes"):::bwBilled
  A -- "CDP 事件" --> S
  A ==> M
  B ==> M
  C ==> M
  D ==> M

  classDef bwSeen fill:#ECFDF5,stroke:#10B981,stroke-width:2px,color:#064E3B
  classDef bwUnseen fill:#F5F3FF,stroke:#A78BFA,stroke-width:1.5px,color:#2E1065
  classDef bwScript fill:#ECFEFF,stroke:#06B6D4,stroke-width:2px,color:#164E63
  classDef bwBilled fill:#4F46E5,stroke:#312E81,stroke-width:2px,color:#FFFFFF
  style Chrome fill:#FAFAFF,stroke:#C4B5FD,stroke-width:1.5px,stroke-dasharray:6 4,color:#4C1D95
  linkStyle 0 stroke:#06B6D4,stroke-width:2px
  linkStyle 1,2,3,4 stroke:#6366F1,stroke-width:2.5px
```

这四类流量都会经过隧道，并计入 `total`。只有第一类会以 CDP 事件的形式到达您的脚本。

### 差异来自哪里

| 来源 | 对您的 CDP 总和与 `total` 的影响 | 在明细中的位置 |
| - | - | - |
| **会话开始前的预热。** 对于部分网站，Browser API 会先加载另一个页面（通常是首页），以便目标页面成功加载。这是我们以高成功率解锁网站的方式之一。预热在您的目标页面加载之前运行，其大小在不同会话之间可能差异很大。 | 您的总和更低 | `before_session`，按资源类型记录在 `before_session_breakdown` 中 |
| **加密开销。** 每个新连接的 TLS 握手，以及每条加密记录的小型头部。 | 您的总和更低 | `overhead_bytes` |
| **Chrome 未通告的请求。** Chrome 有时会在未发送 `Network.requestWillBeSent` 的情况下接收请求数据，例如部分重定向、网站图标以及 Service Worker 请求。 | 您的总和更低 | `client_hidden_bytes` |
| **少量剩余差异（残差）。** 残差的几种可能原因见下文。 | 为正时通常很小；为负时可达百分之几 | 没有单独的字段，按下文方法计算 |

### 哪些数值可以对上

* **精确相等：** `total = payload_bytes + overhead_bytes`，始终成立。
* **精确相等：** `session_breakdown` 中的数值相加等于 `session_bytes`，`before_session_breakdown` 中的数值相加等于 `before_session`。
* **接近，存在少量残差：** `payload_bytes ≈ session_bytes + before_session + client_hidden_bytes`。

比较时请始终计入 `before_session`。预热可能很大，并且在不同会话之间变化明显，但它会完整记录在 `before_session` 中。

### 少量剩余差异（残差）

按如下方式计算残差：

```text theme={null}
residual = payload_bytes − session_bytes − before_session − client_hidden_bytes
```

以上文的示例对象为例：

| | 字节 |
| - | -: |
| `payload_bytes` | 9,667,573 |
| − `session_bytes` | − 2,973 |
| − `before_session` | − 9,567,573 |
| − `client_hidden_bytes` | − 14,201 |
| **= 残差** | **82,826（约 0.9%）** |

**预期情况：**

* **正残差**（`payload_bytes` 高于各字段能解释的数值）：大多数情况下很小。
* **负残差**（基于 CDP 的字段相加超过 `payload_bytes`）：在请求头较多的页面上可达约 10%。这是头部压缩造成的，您按压缩后较小的大小计费。

**对于小页面，请按字节而不是百分比进行比较。** 每个会话有几 KB 的残差是正常的。对于只有几十 KB 的页面，这可能看起来像是很大的百分比。

**残差是什么：** 真实的网络流量，计入 `total`。它不是计量错误：`total = payload_bytes + overhead_bytes` 始终成立。残差的几种可能原因：

* **头部压缩。** HTTP/2 会在网络上压缩头部，而 CDP 报告的是未压缩的头部。这会使您的 CDP 总和\_更高\_，因此残差可能为负。
* **加密分帧和连接消息。** TLS 记录头和 HTTP/2 控制消息（SETTINGS、PING、WINDOW\_UPDATE）属于连接本身，而不属于任何单个请求。
* **流式、长连接或已取消的请求。** Chrome 为这些请求报告的大小可能与实际经过网络的大小不同。
* **请求体。** 上传数据在 CDP 中和线路上的计量方式不同。
* **重传的加密数据**，以及隧道内的 TLS 握手。

以上只是可能的原因。我们不会测量每个会话的具体构成。

**如果残差为正且明显超过百分之几**，请携带 `session_id` 联系支持团队。我们会逐一调查这类情况。

### 客户端常见的统计错误

以下错误会使 CDP 总和偏低，并让残差看起来比实际更大：

* **使用解码后的大小。** `Network.dataReceived.dataLength` 统计的是解压后的响应体，而不是网络上传输的数据。请改用 `Network.loadingFinished` 中的 `encodedDataLength`。
* **只统计响应。** 请求头和 POST 请求体同样是流量。
* **监听器挂载过晚。** 在 `Network.enable` 运行之前发出的请求不会报告给您。
* **只监听一个页面。** iframe、弹出窗口和 Service Worker 可能运行在各自独立的目标中，并有自己的事件。
* **忽略失败的请求。** 被取消或中途失败的请求仍然传输了字节。请同时检查 `Network.loadingFailed`。

## 如何核对您自己的数值

按照以下步骤自行检查一个会话。

1. **使用与我们相同的定义进行统计。** 在首次导航之前挂载监听器，并将请求行、请求头、POST 数据、状态行、响应头和 `encodedDataLength` 相加。下面的示例就是这样做的。
2. **将您的总和与 `session_bytes` 比较。** 两者应当接近。微小差异来自时序以及其他目标中的请求。
3. **计入 `payload_bytes` 的其余部分。** 将 `before_session`（预热）和 `client_hidden_bytes`（Chrome 未通告的请求）加到 `session_bytes` 上。结果应接近 `payload_bytes`：通常只略低于它，在请求头较多的页面上最多高出约 10%。然后加上 `overhead_bytes` 即得到 `total`。
4. **正残差明显超过百分之几？** 请携带 `session_id` 联系支持团队。我们可以详细查看该会话。

### 示例：使用 Puppeteer 统计字节

该脚本像您自己的代码一样根据 CDP 事件统计会话流量，然后将结果与 `Browser.getBandwidth` 返回的带宽明细并排打印，方便您比较两者。

```js theme={null}
'use strict';
const puppeteer = require('puppeteer-core');
const {
    // Replace with your Browser API zone credentials
    AUTH = 'USER:PASS',
    TARGET_URL = 'https://en.wikipedia.org/wiki/Web_scraping',
} = process.env;

const header_bytes = headers=>{
    let size = 0;
    for (const [name, value] of Object.entries(headers||{}))
        size += Buffer.byteLength(`${name}: ${value}\r\n`);
    return size+2;
};

const request_line_bytes = request=>
    Buffer.byteLength(`${request.method} ${request.url} HTTP/1.1\r\n`);

const status_line_bytes = response=>
    Buffer.byteLength(
        `HTTP/1.1 ${response.status} ${response.statusText||''}\r\n`);

const request_body_bytes = request=>{
    if (!request.hasPostData)
        return 0;
    if (Array.isArray(request.postDataEntries))
    {
        return request.postDataEntries.reduce(
            (n, e)=>n+Buffer.from(e.bytes||'', 'base64').length, 0);
    }
    if (request.postData)
        return Buffer.byteLength(request.postData);
    return 0;
};

async function measure(url = TARGET_URL) {
    if (AUTH == 'USER:PASS') {
        throw new Error(`Provide Browser API credentials in AUTH`
            + ` environment variable or update the script.`);
    }
    console.log(`Connecting to Browser...`);
    const browserWSEndpoint = `wss://${AUTH}@brd.superproxy.io:9222`;
    const browser = await puppeteer.connect({browserWSEndpoint});
    try {
        const page = await browser.newPage();
        const client = await page.createCDPSession();

        let sent = 0;
        let received = 0;
        const cached = new Set();
        const req_head_extra = new Set(); // ids with real request headers seen
        const res_head_extra = new Set(); // ids with real response headers seen
        const data_seen = {}; // id -> summed dataReceived bytes
        const finished = new Set(); // ids with a loadingFinished/Failed

        // real on-wire request headers (cookies + browser-added). Prefer this
        // over the cooked requestWillBeSent.headers; may arrive before OR after it.
        client.on('Network.requestWillBeSentExtraInfo', p=>{
            req_head_extra.add(p.requestId);
            sent += header_bytes(p.headers);
        });

        client.on('Network.requestWillBeSent', p=>{
            // a redirect hop folds the previous response into this event
            if (p.redirectResponse && !cached.has(p.requestId))
            {
                received += status_line_bytes(p.redirectResponse)
                    +header_bytes(p.redirectResponse.headers);
            }
            sent += request_line_bytes(p.request)+request_body_bytes(p.request);
            if (!req_head_extra.has(p.requestId))
                sent += header_bytes(p.request.headers); // fallback: cooked
        });

        // raw wire response headers (headersText is the exact blob)
        client.on('Network.responseReceivedExtraInfo', p=>{
            if (cached.has(p.requestId))
                return;
            res_head_extra.add(p.requestId);
            received += p.headersText
                ? Buffer.byteLength(p.headersText) : header_bytes(p.headers);
        });

        client.on('Network.responseReceived', p=>{
            const r = p.response;
            if (r.fromDiskCache || r.fromServiceWorker || r.fromPrefetchCache)
            {
                cached.add(p.requestId);
                return;
            }
            received += status_line_bytes(r);
            if (!res_head_extra.has(p.requestId))
                received += header_bytes(r.headers); // fallback: parsed headers
        });

        // track streamed chunks so an aborted/never-finished body still counts
        client.on('Network.dataReceived', p=>{
            data_seen[p.requestId] =
                (data_seen[p.requestId]||0)+(p.encodedDataLength||0);
        });

        // encodedDataLength = compressed on-wire body size (0 for cache hits)
        client.on('Network.loadingFinished', p=>{
            finished.add(p.requestId);
            if (!cached.has(p.requestId))
                received += p.encodedDataLength||0;
        });

        client.on('Network.loadingFailed', p=>{
            finished.add(p.requestId);
            if (!cached.has(p.requestId))
                received += p.encodedDataLength||0;
        });

        client.on('Network.webSocketFrameSent', p=>{
            sent += p.response?.payloadData
                ? Buffer.byteLength(p.response.payloadData) : 0;
        });

        client.on('Network.webSocketFrameReceived', p=>{
            received += p.response?.payloadData
                ? Buffer.byteLength(p.response.payloadData) : 0;
        });

        await client.send('Network.enable'); // must be before the first navigation

        console.log(`Navigating to ${url}...`);
        await page.goto(url, {waitUntil: 'networkidle2'});

        // add bodies that streamed but never got a loadingFinished/Failed
        for (const [id, bytes] of Object.entries(data_seen))
        {
            if (!finished.has(id) && !cached.has(id))
                received += bytes;
        }

        const my_bytes = sent+received;
        const {bandwidth} = await client.send('Browser.getBandwidth');

        console.log({
            my_sent_bytes: sent,
            my_received_bytes: received, // 0 when page served from warm-up cache
            my_bytes,
            ...bandwidth,
        });
    } finally {
        await browser.close();
    }
}

measure().catch(err=>{
    console.error(err.stack || err);
    process.exit(1);
});
```

这能得到接近但并非完全一致的结果。计费时，始终以 `total` 为准。

## 如何监控和控制带宽

### 按网站跟踪成本

带宽明细按会话提供，因此您可以在此基础上构建按网站的监控：

1. 每个会话结束后，从 Session Logs API 读取 `bandwidth_breakdown`（或在关闭浏览器前通过 CDP 命令读取 `bandwidth`）。
2. 将 `total`、`before_session` 和 `session_bytes` 与目标域名一起存储。
3. 当某个域名每个会话的典型 `total` 大幅上升时发出告警。如果上升由 `before_session` 引起，说明该网站开始运行预热步骤。如果 `session_breakdown` 上升，可以看出是哪种资源类型增长，例如 `image` 或 `media`。

### 设置消费上限

在区域的**高级设置**中设置消费上限。区域达到上限后，新请求将停止。这可以防止区域内所有网站出现意外的用量激增。

### 减少会话内的带宽

您可以控制会话期间加载的内容。拦截不需要的资源类型，例如图片、媒体和字体。请参阅[使用 Browser API 优化带宽使用](https://docs.brightdata.com/cn/products/scraping-browser/code-examples.md#使用-browser-api-优化带宽使用)。使用 `session_breakdown` 查看哪些类型用量最大。拦截 `other` 中的流量时请谨慎：它包括通常用于加载所需数据的 XHR 和 Fetch 调用，拦截后往往会导致页面无法正常工作。

### 关于预热（`before_session`）

预热是 Browser API 以高成功率解锁网站的方式之一。它会自动运行，并且仅在成功加载目标页面需要时才针对相应网站运行。是否预热按网站决定，并可能随着网站调整防护措施而变化。您无法在 CDP 中看到预热，也无法在脚本中将其关闭。禁用验证码解算同样不会关闭预热。

如果某个网站之前没有预热、现在开始预热，或者 `before_session` 随时间增长，这可能是正常现象：预热会随着网站调整防护措施而自适应变化。
