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

# WebSocket latency and completeness: PolyNode, Struct, and RTDS

> Reproducible August 1 and August 4, 2026 benchmarks of pending detection, confirmed fills, and on-chain completeness.

On August 1, 2026, we raced PolyNode, Struct, and Polymarket RTDS from one Node.js process, then checked every eligible result against Polygon logs obtained separately through a benchmark-only external Alchemy RPC. On August 4, we repeated the same 15-minute headline configuration with unchanged collector and analysis source, then ran exact-key and explicit pending-only controls. The two headline windows covered **36,163 transactions and 99,237 fills** across 1,195 fully observed Polygon blocks.

<Info>
  These are PolyNode-run, point-in-time smoke tests. The Alchemy endpoint was used only by the benchmark verifier to enumerate confirmed events and recheck boundary blocks. It is not part of PolyNode's production ingestion, `status_update` generation, or WebSocket delivery path, and it supplied no arrival timestamps used in the latency comparisons. All claims below are limited to the recorded configurations, windows, and network vantage point.
</Info>

<Tip>
  **August 4 retest.** Struct's documented pending path was again not observed: zero pending events in the 15-minute headline run, zero with the exact August 1 credential, and zero under an explicit `status: "pending"` control. PolyNode `status_update` beat Struct confirmed on all 18,122 matched transactions by **9.265 seconds median**, versus 3.963 seconds on August 1—**2.34× the measured August 1 lead**. PolyNode's confirmation timing relative to RTDS moved only 38.5 ms between the two headline windows while Struct's lag behind RTDS grew by 5.370 seconds, evidence of substantial between-window variability in Struct's confirmed-delivery lane.
</Tip>

<CardGroup cols={2}>
  <Card title="No Struct pending—again" icon="ban">
    Four August 4 windows, including exact-key and pending-only controls, produced zero Struct pending events.
  </Card>

  <Card title="9.27-second confirmation lead" icon="bolt">
    PolyNode `status_update` beat Struct confirmed on all 18,122 August 4 transactions.
  </Card>

  <Card title="2.34× the August 1 lead" icon="chart-line">
    The measured confirmation lead grew from 3.963 seconds to 9.265 seconds.
  </Card>

  <Card title="100% confirmed coverage" icon="circle-check">
    PolyNode and Struct each represented all 50,956 August 4 fills after reconciliation.
  </Card>
</CardGroup>

## August 4 retest: pending still not observed

Struct's current [trades-room reference](https://docs.struct.to/websockets/rooms/trades) still defines `status: "all"` as both pending and confirmed, and its [mempool guide](https://docs.struct.to/guides/copy-trading-pending-or-confirmed-trades) says pending events arrive before block confirmation. We reran the published request without a trade-type filter:

```json theme={null}
{
  "room": "polymarket_trades",
  "filters": {
    "status": "all",
    "subscribe_all": true
  }
}
```

The August 4 headline subscription was accepted with `subscribe_all: true`, an empty `rejected` array, and continuous confirmed delivery. It emitted no pending events. The acknowledgment, as on August 1, did not echo the requested `status` field.

### What changed since August 1

Both headline windows used the same host, one-process monotonic clock, source hashes, 12-second warm-up, two-block boundary guard, 30-second confirmation tail, official lowercase RTDS heartbeat, and five-second RTDS silence recovery.

| Metric                                                    | August 1 |     August 4 |               Measured change |
| --------------------------------------------------------- | -------: | -----------: | ----------------------------: |
| Eligible transactions                                     |   18,041 |       18,122 |                           +81 |
| Struct pending events                                     |        0 |            0 |            Still not observed |
| PolyNode `status_update` vs Struct confirmed, median lead | +3.963 s | **+9.265 s** |          **+5.302 s / 2.34×** |
| PolyNode `status_update` first                            |  100.00% |      100.00% |                     Unchanged |
| PolyNode fill vs Struct confirmed record, median lead     | +3.862 s | **+8.888 s** |          **+5.026 s / 2.30×** |
| PolyNode fill first                                       |   99.00% |       98.17% |       -0.83 percentage points |
| PolyNode `status_update` vs RTDS activity, median lead    | -0.171 s |     -0.209 s |                      -0.039 s |
| Struct confirmed vs RTDS activity, median lead            | -4.118 s | **-9.488 s** | 5.370 s additional Struct lag |
| RTDS transaction coverage                                 |  98.537% |      99.559% |      +1.022 percentage points |

A positive lead means the named left-hand path arrived first. The confirmation lead over Struct increased by **133.8%** between the two measured dates. The RTDS control lane helps locate that shift: PolyNode `status_update` versus RTDS moved only 38.5 ms, while Struct confirmed fell another 5.370 seconds behind RTDS. That triangulation supports **substantial between-window variability in Struct confirmed delivery**, not a claim that every future Struct event or region will behave the same way.

### Access and filter controls

The post-run credential audit found that the August 4 headline used a second accepted Struct key rather than the August 1 key. We therefore recovered the exact August 1 credential by its one-way fingerprint and reran the `status: "all"` control. We also ran an explicit `status: "pending"` control. Neither changed the result.

| August 4 window            | Struct credential   | Request             |  Core | Eligible transactions / fills | Struct pending |        Struct confirmed coverage |
| -------------------------- | ------------------- | ------------------- | ----: | ----------------------------: | -------------: | -------------------------------: |
| Initial connection probe   | Second accepted key | `status: "all"`     |  60 s |                 1,385 / 3,652 |              0 |                             100% |
| Explicit pending control   | Second accepted key | `status: "pending"` |  45 s |                   706 / 1,826 |              0 | Not applicable; no Struct events |
| Headline                   | Second accepted key | `status: "all"`     | 900 s |               18,122 / 50,956 |              0 |                             100% |
| Exact August 1 key control | August 1 key        | `status: "all"`     |  60 s |                 1,673 / 4,482 |              0 |                             100% |

Across the four non-overlapping August 4 windows, the independent denominator contained 21,886 transactions and 60,916 fills, while Struct emitted zero pending events. We retain these windows separately and do not pool their latency samples because their credentials, status semantics, and durations differ.

The exact-key control also reproduced the confirmation result: PolyNode `status_update` arrived first for all 1,673 matched transactions by 9.887 seconds median, and PolyNode fills arrived first for all 4,482 reconciled fills by 9.336 seconds median. Both accepted keys returned an empty `rejected` array; the all-status runs continued delivering confirmed data, and no auth, connection-limit, or out-of-credits close was recorded.

<Note>
  The defensible result is that Struct's documented pending path was **not observed again**, across two accepted credentials and an explicit pending-only request. The benchmark cannot identify why the path was absent, and confirmed logs cannot enumerate pending transactions that never reach the chain.
</Note>

### August 4 headline window

| Property                           | Recorded value                                                               |
| ---------------------------------- | ---------------------------------------------------------------------------- |
| Core interval                      | 2026-08-04 01:10:04.218–01:25:04.324 UTC                                     |
| Host clock zone                    | Europe/Berlin, UTC+02:00                                                     |
| Runtime                            | Node.js v24.14.0 on Linux x64                                                |
| Struct SDK                         | 0.6.7                                                                        |
| Eligible blocks                    | 91,400,521–91,401,117 inclusive                                              |
| Boundary hashes                    | `0xc53dbe…c288` through `0x2da733…6192`                                      |
| Core / warm-up / confirmation tail | 900 / 12 / 30 seconds                                                        |
| Chain truth                        | 18,122 transactions / 50,956 `OrderFilled` logs                              |
| Struct request                     | `status: "all"`, `subscribe_all: true`, no trade-type filter                 |
| RTDS heartbeat / recovery          | Official lowercase `ping`; reconnect after 5 seconds without global activity |

The benchmark verifier re-read both boundary blocks after analysis. Their hashes still matched the recorded manifest.

#### Coverage

| Path                             |        Unit |            Seen | Coverage | Missing |
| -------------------------------- | ----------: | --------------: | -------: | ------: |
| PolyNode pending settlement      | Transaction | 18,122 / 18,122 | 100.000% |       0 |
| PolyNode `status_update`         | Transaction | 18,122 / 18,122 | 100.000% |       0 |
| PolyNode confirmed `trade`       |        Fill | 50,956 / 50,956 | 100.000% |       0 |
| Struct pending                   | Transaction |      0 / 18,122 |   0.000% |  18,122 |
| Struct confirmed                 | Transaction | 18,122 / 18,122 | 100.000% |       0 |
| Struct combined confirmed record |        Fill | 50,956 / 50,956 | 100.000% |       0 |
| RTDS activity                    | Transaction | 18,042 / 18,122 |  99.559% |      80 |

Struct emitted 32,834 direct `OrderFilled` records and 18,122 `OrdersMatched` records. All 50,956 records mapped to the chain denominator with zero unmatched records and zero index/order-hash mismatches, so Struct retained 100% confirmed-fill coverage after reconciliation.

#### Latency

Every callback was stamped immediately with `process.hrtime.bigint()` in the same process. A positive lead means the left-hand path arrived first.

| Comparison: left vs right                          | Samples | Median left lead | P95 left lead | Left first |
| -------------------------------------------------- | ------: | ---------------: | ------------: | ---------: |
| PolyNode pending vs PolyNode `status_update`       |  18,122 |         +2.215 s |      +3.332 s |    100.00% |
| ★ **PolyNode `status_update` vs Struct confirmed** |  18,122 |     **+9.265 s** |     +10.845 s |    100.00% |
| PolyNode confirmed fill vs Struct confirmed record |  50,956 |     **+8.888 s** |     +10.698 s |     98.17% |
| ★ **PolyNode pending vs RTDS activity**            |  18,042 |         +2.005 s |      +2.901 s |    100.00% |
| PolyNode `status_update` vs RTDS activity          |  18,042 |         -0.209 s |      +0.415 s |     30.09% |
| Struct confirmed vs RTDS activity                  |  18,042 |     **-9.488 s** |      -8.191 s |      0.00% |

The RTDS row again prevents an incorrect claim: RTDS activity led PolyNode `status_update` by 209 ms median. PolyNode's several-second advantage against RTDS belongs to the pending stream; its 9.265-second confirmed advantage is specifically against Struct confirmed.

#### Adverse observations

Struct arrived first for 934 of 50,956 exact fill pairs across 324 transactions. Every loss was concentrated in the final three eligible blocks, 91,401,115–91,401,117, and the worst PolyNode fill loss was 3.018 seconds. PolyNode `status_update` still beat Struct confirmed for every transaction, including that cluster.

RTDS silently stopped delivering global activity for 5.788 seconds. Its 80 misses occupied exactly four blocks, 91,400,687–91,400,690; activity resumed at block 91,400,691 after the watchdog reconnected and resubscribed. The [80 missing transaction hashes](/assets/case-studies/2026-08-04-websocket-retest/rtds-missing-transactions.json) are published for audit.

The [August 4 compact evidence bundle](/assets/case-studies/2026-08-04-websocket-retest/headline-summary.json) contains the claim/evidence matrix, exact configuration, cross-date metrics, controls, source hashes, block anchors, adverse clusters, integrity results, and SHA-256 inventory for the full retained archives.

## What the August 1 test established

In this window, PolyNode's confirmation signal beat Struct's confirmed signal for all **18,041** matched transactions by **3.963 seconds median**. At exact fill granularity, PolyNode beat Struct on **47,796 of 48,281 fills**, or **98.995%**, by **3.862 seconds median**.

PolyNode pending also arrived before RTDS activity for every one of their **17,777** shared transactions, by **1.984 seconds median**. RTDS was not a complete denominator: even with a five-second silence watchdog and automatic resubscription, it represented **17,777 of 18,041** eligible transactions.

We did **not** find evidence that Struct was missing confirmed transactions or fills. Struct's documented pending path, however, produced no events in the headline run or any retained test window. That result supports the narrow statement that we could not reproduce Struct pending delivery with the documented request on a fresh, credited account. It does not establish why the events were absent.

<Note>
  **No higher-tier requirement found.** [Struct's own benchmark](https://www.struct.to/blog/polymarket-rtds-vs-struct-trades-ws) says that pending mode is included starting on the free plan, while its [trades-room reference](https://docs.struct.to/websockets/rooms/trades) presents `pending` and `all` as ordinary optional status filters. The August 1 subscription, the August 4 headline with a second accepted key, and the August 4 exact-key control were acknowledged with `subscribe_all: true`, an empty `rejected` array, and continued confirmed delivery. We recorded no authentication, connection-limit, or out-of-credits close; [Struct documents](https://docs.struct.to/guides/errors) exhausted credits as close code `4004`. We therefore found no evidence that a paid plan or enhanced key was required. An undisclosed entitlement cannot be ruled out from outside Struct, but it would contradict the published access claim and was not surfaced to the client.
</Note>

## Signal mapping

The word "trade" does not identify the same lifecycle stage in every feed. We fixed the mapping before collecting data.

| Question                 | PolyNode                                                               | Struct                                                                         | RTDS                                                              | Join key                                  |
| ------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------- | ----------------------------------------- |
| Pending detection        | `settlement` with `status=pending`                                     | `received_at` present and `block` absent                                       | `activity/trades` or `activity/orders_matched` as a baseline only | Transaction hash                          |
| Transaction confirmation | `status_update`                                                        | First confirmed record carrying `block`                                        | Activity arrival as a baseline only                               | Transaction hash                          |
| Confirmed fill           | `trade`                                                                | Confirmed `OrderFilled`, plus the corresponding `OrdersMatched` representation | Not used for fill completeness                                    | Transaction hash + canonical log index    |
| Completeness truth       | Polygon `OrderFilled` logs through the benchmark-only Alchemy verifier | Same                                                                           | Same                                                              | Transaction hash + block-global log index |

PolyNode [`status_update`](/websocket/events/status-update) means that a previously pending transaction was confirmed in a Polygon block. It is therefore the transaction-level equivalent used against Struct confirmed. It does not identify every individual fill inside that transaction; PolyNode [`trade`](/websocket/events/trade) is the stricter fill-level comparison. PolyNode [`settlement`](/websocket/events/settlement) with `status=pending` is the pre-confirmation path.

RTDS activity is useful as a public timing baseline, but we do not label it as either pending or confirmed. Its official schema is not an exact semantic equivalent of both stages.

## August 1 headline run

| Property                           | Recorded value                                                    |
| ---------------------------------- | ----------------------------------------------------------------- |
| Core interval                      | 2026-08-01 12:30:08.435–12:45:08.523 UTC                          |
| Host clock zone                    | Europe/Berlin, UTC+02:00                                          |
| Runtime                            | Node.js v24.14.0 on Linux x64                                     |
| Struct SDK                         | 0.6.7                                                             |
| Eligible blocks                    | 91,254,927–91,255,524 inclusive                                   |
| Boundary hashes                    | `0xbb804d…24c2` through `0x6c6430…9a67`                           |
| Core / warm-up / confirmation tail | 900 / 12 / 30 seconds                                             |
| Chain truth                        | 18,041 transactions / 48,281 `OrderFilled` logs                   |
| RTDS heartbeat                     | Official lowercase `ping`, pong-paced                             |
| RTDS recovery                      | Reconnect and resubscribe after 5 seconds without global activity |
| Struct request                     | `status: "all"`, `subscribe_all: true`, no trade-type filter      |

The benchmark verifier re-read both boundary blocks after analysis. Their hashes still matched the recorded manifest.

## August 1 coverage results

Coverage is measured only inside the fully observed block window. Transaction coverage asks whether a feed surfaced a transaction at least once. Fill coverage requires every individual on-chain execution.

| Path                             |        Unit |            Seen | Coverage | Missing |
| -------------------------------- | ----------: | --------------: | -------: | ------: |
| PolyNode pending settlement      | Transaction | 18,041 / 18,041 | 100.000% |       0 |
| PolyNode `status_update`         | Transaction | 18,041 / 18,041 | 100.000% |       0 |
| PolyNode confirmed `trade`       |        Fill | 48,281 / 48,281 | 100.000% |       0 |
| Struct pending                   | Transaction |      0 / 18,041 |   0.000% |  18,041 |
| Struct confirmed                 | Transaction | 18,041 / 18,041 | 100.000% |       0 |
| Struct combined confirmed record |        Fill | 48,281 / 48,281 | 100.000% |       0 |
| RTDS activity                    | Transaction | 17,777 / 18,041 |  98.537% |     264 |

The PolyNode pending row means that every transaction later present in the chain denominator had an earlier PolyNode pending sighting. Confirmed logs cannot prove completeness for pending transactions that never reach the chain.

### Why Struct's strict `OrderFilled` count initially looked low

Struct emitted 30,240 `OrderFilled` records and 18,041 `OrdersMatched` records for the 48,281 chain fills. Counting only its `OrderFilled` type would incorrectly report 62.63% coverage.

For each transaction, Struct's `OrdersMatched` record carried the taker order hash corresponding to one on-chain `OrderFilled`. We fetched every receipt, translated Struct's receipt-local log indexes to Polygon's block-global indexes, and mapped `OrdersMatched` by unique transaction and order hash. All 48,281 records reconciled with:

* zero unmatched records;
* zero index/order-hash mismatches;
* 30,240 direct receipt-index mappings;
* 18,041 unique `OrdersMatched` mappings.

The defensible conclusion is therefore **100% Struct confirmed-fill coverage**, not 62.63%.

## August 1 latency results

Every callback was stamped immediately with `process.hrtime.bigint()` in the same process. A positive lead means the left-hand path arrived first. Missing events never become artificial zero-latency samples.

| Comparison: left vs right                                               | Samples | Median left lead | P95 left lead | Left first |
| ----------------------------------------------------------------------- | ------: | ---------------: | ------------: | ---------: |
| PolyNode pending vs PolyNode `status_update`                            |  18,041 |         +2.148 s |      +3.189 s |    100.00% |
| ★ **PolyNode block confirmation (`status_update`) vs Struct confirmed** |  18,041 |         +3.963 s |      +5.083 s |    100.00% |
| PolyNode confirmed fill vs Struct confirmed record                      |  48,281 |         +3.862 s |      +5.061 s |     99.00% |
| ★ **PolyNode pre-confirmation (pending) vs RTDS activity**              |  17,777 |         +1.984 s |      +2.858 s |    100.00% |
| PolyNode block confirmation (`status_update`) vs RTDS activity          |  17,777 |         -0.171 s |      +0.428 s |     34.48% |
| Struct confirmed vs RTDS activity                                       |  17,777 |         -4.118 s |      -2.964 s |      0.00% |

★ The two primary comparisons: PolyNode's confirmed-transaction signal against Struct confirmed, and PolyNode's pre-confirmation signal against RTDS activity.

The RTDS comparison is an important check against overclaiming. RTDS activity arrived **171 ms before** PolyNode `status_update` at the median and won 65.52% of their overlaps. It is therefore wrong to describe PolyNode confirmation as several seconds ahead of RTDS. The several-second confirmed advantage is against Struct; the PolyNode advantage against RTDS belongs to the pending signal.

### Adverse fill windows

Struct arrived first for 485 fills across 203 transactions. Those losses were concentrated in six blocks: 91,254,964–91,254,965, 91,255,009–91,255,010, and 91,255,121–91,255,122. The worst observed PolyNode loss was 2.187 seconds.

Outside those blocks, PolyNode won every exact fill pair. We include the clusters because a median alone would hide them.

## August 1 RTDS continuity

The RTDS socket silently stopped delivering global activity three times without first producing an error or close event. The watchdog terminated the open socket and resubscribed each time.

| Stall |       Last observed block | First block after recovery | Missing eligible blocks | Missing transactions | Activity gap |
| ----: | ------------------------: | -------------------------: | ----------------------- | -------------------: | -----------: |
|     1 |                91,255,272 |                 91,255,277 | 91,255,273–91,255,276   |                  187 |      7.204 s |
|     2 |                91,255,476 |                 91,255,481 | 91,255,477–91,255,480   |                   77 |      6.966 s |
|     3 | After the eligible window |  After the eligible window | None eligible           |                    0 |      6.656 s |

The final RTDS coverage was therefore 98.537%, not 100%. The [264 missing transaction hashes](/assets/case-studies/2026-08-01-websocket-benchmark/rtds-missing-transactions.json) are published as a machine-readable audit file.

## Exact collection method

### Requests sent

The final run used these literal harness excerpts. `args.structStatus` was `all`, and `args.tradeTypes` was unset.

<Tabs>
  <Tab title="Struct">
    ```ts theme={null}
    const filters: Record<string, unknown> = {
      status: args.structStatus,
      subscribe_all: true,
    };
    if (args.tradeTypes?.length) filters.trade_types = args.tradeTypes;
    const ack = await socket.subscribe("polymarket_trades", filters as never);
    ```
  </Tab>

  <Tab title="PolyNode">
    ```ts theme={null}
    const request = { action: "subscribe", type: "trades" };
    socket.send(JSON.stringify(request));
    ```
  </Tab>

  <Tab title="Polymarket RTDS">
    ```ts theme={null}
    const request = {
      action: "subscribe",
      subscriptions: [
        { topic: "activity", type: "trades" },
        { topic: "activity", type: "orders_matched" },
      ],
    };
    current.send(JSON.stringify(request));
    ```
  </Tab>
</Tabs>

Struct acknowledged `subscribe_all: true`, returned an empty `rejected` array, and streamed confirmed records. Its acknowledgment did not echo the requested `status` field.

<Accordion title="Run the PolyNode side">
  This minimal Node.js client uses the same `trades` subscription and keeps the three lifecycle signals separate. See [WebSocket subscriptions](/websocket/subscribing) for server-side filters.

  ```bash theme={null}
  npm install ws
  ```

  ```js theme={null}
  import WebSocket from "ws";

  const apiKey = process.env.POLYNODE_API_KEY;
  if (!apiKey) throw new Error("Missing POLYNODE_API_KEY");

  const endpoint = new URL("wss://ws.polynode.dev/ws");
  endpoint.searchParams.set("key", apiKey);

  const ws = new WebSocket(endpoint);

  ws.on("open", () => {
    ws.send(JSON.stringify({ action: "subscribe", type: "trades" }));
  });

  ws.on("message", (raw) => {
    const message = JSON.parse(raw.toString());
    const items = Array.isArray(message.events) ? message.events : [message];

    for (const item of items) {
      const event = item?.data ?? item;
      if (!event || typeof event !== "object") continue;

      if (event.event_type === "settlement" && event.status === "pending") {
        console.log("pending", event.tx_hash);
      } else if (event.event_type === "status_update") {
        console.log("confirmed transaction", event.tx_hash);
      } else if (event.event_type === "trade") {
        console.log("confirmed fill", event.tx_hash, event.log_index);
      }
    }
  });
  ```
</Accordion>

### Immediate, same-clock stamping

```ts theme={null}
function now(): Stamp {
  return { monoNs: process.hrtime.bigint().toString(), wallMs: Date.now() };
}

socket.on("message", (raw: RawData) => {
  const stamp = now();
  const text = raw.toString();
  // Parsing and normalization follow the arrival stamp.
});
```

Wall time remains in the artifacts for human auditing. All latency math uses the monotonic nanosecond value.

### Lifecycle classification

```ts theme={null}
const isPending = event.received_at != null && event.block == null;
recorder.event(
  "struct",
  isPending ? "pending" : "confirmed",
  "trade_stream_update",
  {
    tx_hash: normalizeHash(event.hash),
    log_index: numeric(event.log_index),
    block_number: numeric(event.block),
    order_hash: normalizeHash(event.order_hash),
    trade_type: typeof event.trade_type === "string"
      ? event.trade_type
      : undefined,
    status: isPending ? "pending" : "confirmed",
  },
  stamp,
);

if (eventType === "settlement") {
  recorder.event(
    "polynode",
    fields.status === "pending" ? "pending" : "settlement_other",
    eventType,
    fields,
    stamp,
  );
} else if (eventType === "status_update") {
  recorder.event("polynode", "confirmed", eventType, fields, stamp);
} else if (eventType === "trade") {
  recorder.event("polynode", "confirmed_fill", eventType, fields, stamp);
}
```

This preserves the pending/confirmed classification in [Struct's published benchmark](https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark) while keeping PolyNode's transaction confirmation separate from its individual fills.

### Independent denominator and block boundaries

After all three sockets were active and the 12-second warm-up finished, the harness read the Polygon tip and armed the first eligible block at `tip + 2`. At core completion, it ended the denominator at `tip - 2`, then kept sockets open for 30 seconds.

The harness queried every known current and legacy Polymarket CLOB exchange for `OrderFilled` logs. It then fetched every eligible receipt. The headline window happened to contain only V2 fills: 39,150 from the standard exchange and 9,131 from the neg-risk exchange.

Each confirmed fill was keyed as:

```ts theme={null}
function fillKey(txHash: string, logIndex: number): string {
  return `${txHash}:${logIndex}`;
}

// Exact latency field used inside the sample object:
left_lead_ms: Number(rightNs - leftNs) / 1_000_000,
```

Using only transaction hash would have hidden missing fills because every transaction in this window contained multiple `OrderFilled` logs.

### Reproduction command

```bash theme={null}
npm ci
npm test
npm run check

STRUCT_API_KEY='<redacted>' \
POLYNODE_API_KEY='<redacted>' \
POLYGON_RPC_URL='<redacted>' \
npm run bench -- \
  --duration-sec 900 \
  --warmup-sec 12 \
  --tail-sec 30 \
  --struct-status all \
  --rtds-heartbeat polymarket-lowercase \
  --rtds-silence-reconnect-sec 5 \
  --out results/headline-15m-2026-08-01
```

Credentials are read only from environment variables. The artifacts contain redacted endpoints and short one-way fingerprints, never raw keys.

## August 1 control runs and prior windows

We retained ten non-overlapping windows totaling 1,995 seconds, 39,992 confirmed transactions, and 107,210 chain fills. Struct emitted zero pending events in every retained window.

| Phase                          | Windows | Core time | Transactions |  Fills | Struct pending | Purpose                                                 |
| ------------------------------ | ------: | --------: | -----------: | -----: | -------------: | ------------------------------------------------------- |
| Instrumentation smokes         |       2 |      60 s |          871 |  2,433 |              0 | Validate subscriptions, normalization, and boundaries   |
| Exact Struct-methodology suite |       5 |     675 s |       13,595 | 36,466 |              0 | Preserve Struct's uppercase `PING` harness behavior     |
| Official-heartbeat control     |       1 |     180 s |        3,673 |  9,832 |              0 | Replace uppercase `PING` with official lowercase `ping` |
| Recovery shakedown             |       1 |     180 s |        3,812 | 10,198 |              0 | Validate a 10-second silence watchdog                   |
| Headline run                   |       1 |     900 s |       18,041 | 48,281 |              0 | Official heartbeat plus five-second recovery            |

The five-window exact-methodology suite produced these pooled results:

* PolyNode and Struct each covered 100% of 13,595 confirmed transactions and 36,466 reconciled fills.
* PolyNode `status_update` beat Struct confirmed by 4.017 seconds median and arrived first for every transaction.
* PolyNode fills beat Struct records by 3.955 seconds median and arrived first for 95.55% of fills.
* PolyNode pending beat every RTDS overlap by 1.995 seconds median.
* RTDS transaction coverage was 83.96% because two later sockets stayed open but stopped delivering activity.

One exact-methodology window contained a nine-block PolyNode fill-latency cluster: PolyNode won 80.87% of fills in that window while retaining 100% coverage. The other four exact windows had PolyNode first on 100% of fills.

The official lowercase heartbeat did not by itself prevent RTDS silence. In its isolated 180-second control, RTDS covered 56.00% of eligible transactions and stopped for the final 52 blocks. Recovery improved coverage to 93.94% with a ten-second watchdog and 98.54% with the final five-second watchdog, but WebSocket resubscription cannot replay events lost during the gap.

An earlier exploratory `status: "pending"`-only probe on the fresh credited Struct account also produced no pending events while RTDS remained active. Because that exploratory probe did not use the final artifact schema, it is recorded as diagnostic context and excluded from every scored table.

## Artifacts and integrity

The [August 4 compact retest bundle](/assets/case-studies/2026-08-04-websocket-retest/headline-summary.json) contains the claim/evidence matrix, headline and control metrics, exact configurations, credential fingerprints, cross-date comparison, lifecycle gap, adverse blocks, source hashes, block anchors, and SHA-256 inventory. The separate [RTDS missing-transaction file](/assets/case-studies/2026-08-04-websocket-retest/rtds-missing-transactions.json) contains all 80 identifiers from the retest gap.

The full August 4 headline archive is 493 MB and contains:

* 226,513 normalized events;
* 226,528 parsed raw frames;
* 174,161 latency CSV lines, including the header;
* every Polygon `OrderFilled` log and receipt needed for reconciliation;
* exact missing identifiers and connection lifecycle events;
* readable and machine-readable summaries plus verified SHA-256 checksums.

The [August 1 compact result bundle](/assets/case-studies/2026-08-01-websocket-benchmark/headline-summary.json) preserves the original configuration, results, and archive checksums. The [credential-free source bundle](/assets/case-studies/2026-08-01-websocket-benchmark/harness-source.json) contains the complete harness, analyzer, tests, dependency lock, README, and license. Its source hashes match the August 4 collector exactly.

For the August 4 retest, the TypeScript compiler and all three analysis regression tests passed. All JSON and NDJSON artifacts parsed, all checksums verified, all 188 RPC audit calls succeeded, both boundary hashes rechecked, and all 50,956 Struct records reconciled with zero unmatched records or identity mismatches. An exact-value scan found none of the tested credentials in source or publishable artifacts.

## Limitations

* These are two dates from one network vantage point. They do not guarantee the same latency from every region or in future provider versions.
* Two headline windows establish a large measured shift but do not estimate Struct's universal latency variance or justify saying every future Struct window will be inconsistent.
* The August 4 headline used a second accepted Struct credential. The exact August 1 credential was retested separately and reproduced zero pending delivery and similar confirmed latency; the separate samples are not pooled.
* The traffic was live, not a controlled synthetic workload. Market activity and block density varied.
* On-chain logs prove confirmed completeness. They cannot enumerate pending transactions that never confirm.
* RTDS is a public activity baseline, not a semantic clone of either pending or confirmed streams.
* A five-second watchdog trades faster recovery for more reconnects. Different thresholds change the size of unrecoverable gaps.
* Provider-supplied timestamps were retained but never used for cross-provider latency because their clock origins are not comparable.

## References

* [Struct's published benchmark methodology](https://www.struct.to/blog/polymarket-rtds-vs-struct-trades-ws)
* [Struct's published benchmark source](https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark)
* [Struct trade room documentation](https://docs.struct.to/websockets/rooms/trades)
* [Struct pending and confirmed copy-trading guide](https://docs.struct.to/guides/copy-trading-pending-or-confirmed-trades)
* [Polymarket official real-time data client](https://github.com/Polymarket/real-time-data-client)
* [PolyNode WebSocket overview](/websocket/overview)

Last evidence audit: **2026-08-04 01:49 UTC**.
