Duplicates¶
duplicates lists requests that were made more than once. It groups them by a fingerprint and
tags the groups in which a failure was followed by another try. Use it to see what repeats, then decide
for each group whether the repeat is waste, a retry, or normal.
12x GET api.shop.example/v1/products/{id}/price
statuses: 200:12
entries: e000026, e000027, e000028, e000029, e000030, e000031, e000032, e000033, e000034, e000035, e000036, e000037
12x POST api.shop.example/v1/session/ping
statuses: 200:12
entries: e000038, e000041, e000042, e000050, e000052, e000055, e000058, e000060, e000062, e000063, e000065, e000068
6x GET api.shop.example/v1/flags
statuses: 200:6
entries: e000019, e000040, e000051, e000061, e000066, e000071
6x GET cdn.shop.example/img/hero.png
statuses: 302:6
entries: e000007, e000020, e000039, e000053, e000067, e000074
5x [retry pattern] POST api.shop.example/v1/cart/sync
statuses: 500:1 503:4
entries: e000069, e000070, e000072, e000073, e000076
4x GET api.shop.example/v1/cart
statuses: 200:2 401:2
entries: e000012, e000013, e000017, e000085
3x POST api.shop.example/graphql
statuses: 200:3
entries: e000082, e000083, e000084
3x POST api.shop.example/v1/track
statuses: 200:3
entries: e000093, e000094, e000095
3x POST o0.ingest.sentry.io/api/{id}/envelope/
statuses: 200:3
entries: e000021, e000057, e000075
2x GET api.shop.example/v1/auth/session
statuses: 200:1 401:1
entries: e000011, e000016
What a group is¶
Two requests are in the same group when they have the same:
- HTTP method,
- host,
- normalized path, where ids become
{id}, and - query string, without the cache-buster keys
_,cb,cachebuster,nonce,tsandtimestamp.
The body is not part of the fingerprint. Two POSTs to the same URL with different bodies are one
group. Use diff to see whether the bodies differ.
Groups need at least two requests and are sorted by size. --top bounds the list, with a default of 10.
Read a group¶
| Part | Meaning |
|---|---|
12x |
The number of requests in the group. |
[retry pattern] |
After a request failed with a 5xx, a 429 or no status, a later request in the group tried again. See Retries. |
statuses |
How the requests ended. 200:2 401:2 is two 200s and two 401s. |
entries |
The ids, in time order. |
Not every duplicate is waste¶
A group says "these look alike", not "this is wrong". Each group in the capture needs a different reading:
| Group | What it is |
|---|---|
12x GET /v1/products/{id}/price |
Twelve different products, folded into one by normalization. It is an N+1, and the fix is one request for all twelve. |
12x POST /v1/session/ping |
A heartbeat. The calls are identical except for a timestamp. See Remove redundant calls. |
6x GET /v1/flags |
The same call with a different cb each time. The key is ignored, so they group. Caching would remove them. |
6x GET /img/hero.png |
The same redirect, six times. See Redirects. |
5x [retry pattern] POST /v1/cart/sync |
A retry loop. See Retries. |
4x GET /v1/cart |
Two 401s and two 200s, over 70 seconds. Calls that happen minutes apart are usually legitimate. |
3x POST /graphql |
One endpoint with three different queries. A GraphQL or RPC endpoint always looks like a duplicate, because the body is not compared. |
Look at the spread in time as well as the count. Twelve calls in one second is a burst. Twelve calls
spread over a minute is a rhythm. timeline shows which, and storms finds bursts.
Narrow it¶
12x POST api.shop.example/v1/session/ping
statuses: 200:12
entries: e000038, e000041, e000042, e000050, e000052, e000055, e000058, e000060, e000062, e000063, e000065, e000068
5x [retry pattern] POST api.shop.example/v1/cart/sync
statuses: 500:1 503:4
entries: e000069, e000070, e000072, e000073, e000076
3x POST api.shop.example/graphql
statuses: 200:3
entries: e000082, e000083, e000084
JSON¶
wiretrail checkout.har duplicates --top 1 --json | jq '.result.groups[0] | {fingerprint, count, statuses, is_retry_pattern, first_offset_ms, last_offset_ms}'
{
"fingerprint": "GET api.shop.example /v1/products/{id}/price ",
"count": 12,
"statuses": {
"200": 12
},
"is_retry_pattern": false,
"first_offset_ms": 4200.0,
"last_offset_ms": 4860.0
}
The fingerprint ends with the query pairs, so it ends in a space when there are none.
Exit code¶
duplicates exits 1 when it finds at least one group and 0 when it finds none.
Related¶
- What varies between calls tells a pure duplicate from a call whose body changes.
- Retries covers the groups tagged
[retry pattern]. - Storms finds bursts.