Due modalità headless, una sola reputazione
Fino a Chrome 112, --headless significava il vecchio headless: un percorso binario separato che non era davvero Chrome. Aveva uno user agent suo contenente la stringa HeadlessChrome, nessun supporto alle estensioni, nessun chrome.runtime, nessun elenco di plugin e un percorso di rendering che saltava grandi parti della pipeline reale. Rilevarlo era una riga di codice.
Chrome 112 ha introdotto il nuovo headless (--headless=new, e da Chrome 132 semplicemente --headless). È il browser vero con la finestra non disegnata. Le estensioni funzionano, l'elenco dei plugin è popolato, chrome.runtime esiste e lo user agent non si annuncia più. Ha chiuso quasi ogni segnale che perdeva la vecchia modalità.
La reputazione è rimasta comunque, e in gran parte a ragione. Il nuovo headless ha rimosso le tracce facili. Quello che non ha rimosso è lo strato sotto di esse.
Cosa lo tradisce ancora
SwiftShader, quello che chiude la sessione
Il segnale più affidabile in assoluto, e quello su cui inciampano quasi tutti. Senza GPU — che è lo stato normale di un server — Chrome ripiega sulla rasterizzazione software, e WebGL riporta:
UNMASKED_VENDOR_WEBGL → "Google Inc. (Google)"
UNMASKED_RENDERER_WEBGL → "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device ...))"
Quella coppia esiste su zero dispositivi consumer. Non "raramente": zero. Nessuno naviga su Instagram con il rendering software. Non è un segnale probabilistico che alza un punteggio; è un segnale categorico che chiude la sessione.
È anche il motivo per cui --disable-gpu è un consiglio pessimo in questo caso d'uso. Il flag viene copiato in massa da thread di risoluzione problemi su Docker dove risolve un crash, e forza esattamente il ripiego di cui sopra.
La finestra che non c'è
Headless deve comunque inventarsi una finestra, e l'invenzione si vede. outerWidth e outerHeight spesso coincidono con innerWidth e innerHeight, perché non c'è chrome di cui tenere conto — un Chrome desktop vero ha circa 16 pixel di differenza in orizzontale e 85-90 in verticale. screenX e screenY stanno a zero. screen.availHeight è uguale a screen.height, il che significa che su quella macchina non esiste da nessuna parte una barra delle applicazioni o un dock.
Non si muove mai niente
Nessun movimento del mouse, nessuna inerzia dello scorrimento, nessun focus e blur mentre l'utente cambia finestra, nessun visibilitychange quando una scheda passa in secondo piano. Cloudflare Turnstile e i suoi pari osservano queste cose di continuo, non come singolo controllo. Una sessione con un fingerprint pulito e zero eventi di input è una sessione che ha superato ogni test statico e fallito l'unico dinamico.
Lo stack sottostante
Headless non cambia i fingerprint TLS o HTTP/2 — il che è una buona notizia sul serio, dato che una build Chromium vera ti dà handshake da Chrome veri in entrambi i casi. Non cambia nemmeno la superficie CDP, che è dove l'automazione viene beccata davvero. Cambiare modalità non sposta né l'una né l'altra.
Perché i plugin stealth hanno smesso di funzionare
puppeteer-extra-plugin-stealth e i suoi discendenti applicano una serie di patch JavaScript all'avvio del documento: nascondono webdriver, popolano navigator.plugins, falsificano chrome.runtime e così via. Nel 2019 spesso bastava.
Oggi non basta più, per una ragione strutturale e non di manutenzione. Ogni patch è un override JavaScript, e gli override JavaScript hanno una forma. I getter nativi si stringificano come [native code]; i sostituti no. Le proprietà del prototipo non compaiono come proprietà proprie; i sostituti sì. E nessuno script a livello di pagina raggiunge lo scope di un Web Worker, quindi qualsiasi valore corretto sul thread principale non concorda con la copia del worker.
I benchmark di rilevamento pubblicati nel 2026 riportano un'identificazione quasi totale delle sessioni con plugin stealth — un fornitore indica Playwright puro al 98,2% e le sessioni commerciali browserless in modalità stealth al 100%, con tassi di falsi positivi sotto l'1%. Quei numeri vengono da un'azienda che vende sistemi di rilevamento e vanno letti tenendolo presente, ma la direzione non è in discussione.
I plugin non sono scritti male. Stanno lavorando a un livello che non può esprimere la risposta.
L'opzione che non è nel menu
L'alternativa "headless o headful" nasconde la scelta che funziona davvero: Chrome headful su un display virtuale.
Su Linux, esegui Xvfb — un vero X server che renderizza in memoria invece che su un monitor. Chrome parte senza alcun flag --headless. È un browser completo, che disegna una finestra completa, con rendering GPU reale se l'host ne ha una. Non c'è alcuna modalità headless da rilevare perché non c'è alcuna modalità headless.
Xvfb :99 -screen 0 1920x1080x24 &
DISPLAY=:99 google-chrome --window-size=1920,1080
Cosa risolve: il segnale SwiftShader (con una GPU presente), i segnali sulla geometria della finestra, la chrome mancante, availHeight e l'intera classe di tracce del tipo "non c'è nessun display". Cosa non risolve: i leak di CDP e il comportamento. Quelli richiedono lavoro a parte.
Il costo sono qualche decina di megabyte di RAM per istanza e una riga nel tuo script di provisioning. Rispetto a mantenere un set di patch stealth contro un bersaglio in movimento, è quasi gratis.
Quando headless è ancora la scelta giusta
Niente di tutto questo rende headless inutile. Lo rende lo strumento sbagliato per un compito specifico.
Headless va bene per: test interni, generazione di screenshot e PDF, crawling di siti tuoi, scraping di siti senza difese anti-bot, pipeline di CI, controllo dei link, monitoraggio dell'uptime. Ovunque nessuno stia cercando di identificarti, headless è più veloce e più leggero e dovresti usarlo.
Headless è lo strumento sbagliato per: qualsiasi cosa dietro Cloudflare, Akamai, DataDome o PerimeterX; qualsiasi cosa che coinvolga un account loggato su una piattaforma a cui la cosa interessa; qualsiasi cosa in cui essere classificato come automazione abbia un costo.
La distinzione non è il livello di raffinatezza tecnica. È se c'è un avversario.
Una breve diagnosi
Se vuoi sapere a che punto è la tua configurazione attuale, quattro controlli in ordine di quanto ti dicono:
- Renderer WebGL. Leggi
UNMASKED_RENDERER_WEBGL. Se nomina SwiftShader, fermati e sistema quello prima di ogni altra cosa; niente altro conta. - Geometria della finestra.
outerHeight - innerHeightdovrebbe essere circa 85-90 su Chrome desktop, escreen.availHeightdovrebbe essere più piccolo discreen.height. - CreepJS. Caricalo e leggi le sezioni su headless e sul rilevamento delle bugie. È brutale su ciò che trova.
- Un bersaglio vero. Carica un sito che ti interessa davvero e vedi se ti arriva una sfida. Ogni test sintetico è un surrogato di questo.
Esegui gli stessi quattro controlli su un browser che guidi a mano sulla stessa macchina. Le differenze fra i due elenchi sono la tua lista di cose da fare.