pixellint

Pixels · engineers · marketers

An image cannot hash an email

The img tag is a GET with a URL. No cookies you did not already have on that request, no SHA-256 in the browser, no JSON body. JavaScript tags exist because vendors needed those things. URL length is the image's ceiling. CSP script-src is the script's.

What the image gets you

Works with JavaScript disabled, works in many email clients, and is easy to traffic as a 1x1 in an ad server. The browser applies image-src, not script-src. There is no event queue, no consent stub to wait on inside the tag, and no chance the library initializes twice. The collector sees method GET, a query string, and whatever cookies the browser still attaches to that host.

You also get every GET limitation. No request body. No custom header. No SHA-256 of a normalized email computed on the device. If the vendor's match quality wants hashed em, an image cannot produce it unless some server hashed it first and stuffed the hex into the URL. That hex still counts against the length cap.

The two thousand character ceiling

Browsers and intermediaries cap GET URLs somewhere around two thousand characters in practice. IE's historic 2083 is the number people still cite. Chromium is more generous until a proxy, a CDN, or the vendor's own front door is not. A 414 URI Too Long is the honest failure. Silent truncation of the last query pair is the dishonest one: the request returns 200 and cd[content_ids] is missing the last SKU.

Stuffing a hashed email, a click id, a product catalog, and a long event_source_url into one query string is how you mint that 414. Image pixels are the wrong transport for a 40-line basket. Split the catalog, use the JS tag's POST, or send the conversion server-side. Do not gzip the query; collectors will not inflate it.

Floodlight and LinkedIn still ship images

Campaign Manager activity tags on doubleclick.net are semicolon-delimited path parameters on an image GET: src (numeric Floodlight configuration ID), type (activity group), cat (activity tag), and ord (cache buster). Unique counting also wants num alongside ord. That is an official image contract, not a legacy you should rewrite as fetch.

LinkedIn Insight fallbacks still look like px.ads.linkedin.com/collect?pid=...&fmt=gif. pid is the partner id. fmt=gif asks for a 1x1. Treat that URL as an image pixel when you traffic it, and as a different artifact from LinkedIn's Conversions API JSON. Mixing the gif URL into a POST body is not a documented upgrade.

fbq, gtag, and ttq POST

Meta's fbq, Google's gtag, and TikTok's ttq are JavaScript tags. They read document.cookie, they queue events until the library loads, they can wait on a consent update, and they can POST a body that does not fight the GET length cap. facebook.com/tr still appears as a GET from fbq for many events; the point is the library, not that every call is POST. gtag's collect hit is GET to /g/collect with v=2, tid starting G-, cid, and en. The library is what computes those fields.

The cost is a third-party origin executing on your page. The tag can fire twice if you initialize twice. It can miss a SPA route if nobody hooks history. It can hash PII in the browser, which is why an image-only Meta pixel will never match on em the way the JS tag can. Use the image as noscript fallback, not as the only production path, if the vendor's optimization wants advanced matching.

CSP, and do not mix them blindly

Content-Security-Policy script-src must allow the tag host (connect.facebook.net, www.googletagmanager.com, analytics.tiktok.com) or the JS never runs. img-src must allow the image host (www.facebook.com, ad.doubleclick.net, px.ads.linkedin.com) or the 1x1 never loads. A policy that allows the image and blocks the script leaves you on noscript. A policy that allows the script and blocks the image leaves you dark when JS is off. connect-src matters for fetch and sendBeacon; forgetting it looks like an ad blocker in the Network tab.

Shipping both the JS tag and the image without deduplication double-counts. Shipping only the image when the vendor wants hashed PII leaves match quality on the table. Shipping only the script with no noscript means you go dark for users who blocked JS. Meta's image path takes noscript=1 so the collector can tell the two transports apart. That flag is not a reason to fire the image on every JS hit.

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.