JA4 splits the fingerprint into three parts:
t13d190900_5d65cb28da5c_02713d6af862
│ │ │
a b c
a = protocol + TLS version + counts + ALPN
b = cipher hash (changes on rotation)
c = extension hash
When the service rotates ciphers, only b changes. a and c stay constant.
the fix
Instead of grouping by full fingerprint, group by ac:
def normalize_fingerprint(fp):
parts = fp.split('_')
return f"{parts[0]}_{parts[2]}" # ignore cipher part
# Before: 50k unique fingerprints
# After: 47 unique fingerprints
Cardinality back to normal. Metrics useful again.
when this matters
If you track client types in metrics and see unexplained cardinality spikes, check if they're rotating ciphers. The a_b_c format lets you ignore the changing parts.
Saved us from having to increase our Prometheus retention limits.