k6: il load testing che i tuoi unit test non possono fare

Immagine di copertina

La pipeline è verde. Test unitari passati, code review approvata, deploy in produzione senza intoppi.

Poi arriva il primo vero picco di traffico (una campagna marketing, un lancio, la stagione alta) e l’endpoint che rispondeva in 50 millisecondi ora ne impiega 3000. Gli utenti si lamentano, i log non dicono granché, e tu ti chiedi come sia possibile che nessun test l’abbia previsto.

Semplice: nessun test lo stava cercando.

Il codice era corretto. Il problema non era una funzione che restituiva il valore sbagliato, ma il sistema nel suo complesso che non reggeva più di tot richieste alla volta. Ed è un tipo di bug che nessun test unitario è progettato per trovare.


Il gap che i test tradizionali non chiudono

I test unitari mockano il database: non c’è un connection pool da saturare, non c’è latenza di rete, non c’è contesa sulle risorse. Verificano che la logica sia corretta, non come si comporta sotto pressione.

Il profiler locale ha lo stesso limite: misura un utente alla volta, sul tuo computer, con una sola richiesta in transito. I problemi di concorrenza restano invisibili finché non ci sono davvero tanti utenti insieme: query che si accodano, lock che si allungano, pool che si esauriscono.

Non sono strumenti sbagliati. Testano esattamente quello per cui sono stati costruiti. Il problema è che nessuno di questi strumenti simula il carico, e senza carico, certe situazioni semplicemente non si presentano.

Pensa a una query che con un utente gira in 5ms perché tutto è in cache e nessuno le contende l’accesso al database. Con 100 utenti concorrenti la stessa query si accoda, il connection pool va in saturazione, e il tempo di risposta esplode. Nessuna riga di codice è cambiata: è cambiato solo il numero di richieste simultanee.

Il load testing colma proprio questo gap: genera traffico concorrente realistico e osserva come si comporta il sistema nel suo complesso, non una funzione isolata.


Cos’è k6

k6 è uno strumento di load testing open source di Grafana Labs. Gli script si scrivono in JavaScript, girano da riga di comando (o in Docker), e non aprono un vero browser: k6 genera direttamente richieste HTTP (o WebSocket, gRPC) a bassissimo overhead. Puoi simulare centinaia o migliaia di utenti concorrenti da una singola macchina, cosa impensabile con strumenti che replicano un browser reale per ogni utente virtuale.

Rispetto ad alternative come JMeter (basato su XML/GUI, pesante) o Locust (Python, basato su gevent e quindi single-process, con scaling limitato a meno di non orchestrare più worker), k6 sfrutta le goroutine di Go: ogni virtual user è leggerissimo, e uno script scala a migliaia di utenti su più core senza saturare la tua macchina.

k6 non serve solo per lo stress test finale prima del rilascio. Uno script di poche righe puoi tenerlo anche nella pipeline CI, come guardia contro regressioni di performance introdotte da una PR apparentemente innocua.

Non tutti i load test hanno lo stesso obiettivo. I pattern più comuni:

  • Smoke test: pochi virtual user, giusto per verificare che lo script funzioni e il sistema risponda sotto carico minimo
  • Load test: carico atteso in condizioni normali, per verificare che il sistema regga il traffico previsto
  • Stress test: carico crescente oltre l’atteso, per trovare il punto di rottura
  • Soak test: carico moderato ma prolungato nel tempo, per scovare memory leak o degradi lenti

Per iniziare, un load test semplice basta: simuli il traffico atteso e verifichi che il sistema lo regga entro le soglie che ti interessano.

Installarlo richiede un comando:

brew install k6
# oppure, senza installare nulla:
docker run --rm -i grafana/k6 run - <script.js

Il tuo primo script

Uno script k6 minimo ha due parti: la funzione che descrive cosa fa ogni virtual user, e le options che descrivono quanto carico generare.

// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 50,          // virtual users concorrenti
  duration: '30s',  // durata del test
};

export default function () {
  const res = http.get('https://api.tuosito.dev/products');

  check(res, {
    'status è 200': (r) => r.status === 200,
  });

  sleep(1); // think time tra una richiesta e l'altra
}

vus: 50 dice a k6 di simulare 50 utenti che eseguono la funzione default in loop per 30 secondi. Il sleep(1) non è cosmetico ma serve a creare un pattern più simile al comportamento di un utente, in sua assenza ogni virtual user invierebbe ogni richiesta alla massima velocità.

La funzione check() non fa fallire il test se una condizione è falsa: registra solo una percentuale di successo nel report finale. È il modo in cui k6 distingue “il server ha risposto” da “il server ha risposto correttamente”: un endpoint può rispondere velocemente con un errore 500, e senza un check() esplicito non è possibile accorgersene guardando solo i tempi di risposta.

Esegui lo script con:

k6 run script.js

E k6 comincia a generare traffico, mostrando un riepilogo live in terminale.


Leggere i risultati

Al termine, k6 stampa un report con le metriche chiave. Le tre più importanti sono:

  • http_req_duration: quanto tempo impiega ogni richiesta, con percentili (p90, p95, p99). Il p95 è quello che conta di più: dice quanto è lenta l’esperienza per il 5% degli utenti più sfortunati, non solo la media
  • http_req_failed: percentuale di richieste fallite (timeout, errori 5xx, connessioni rifiutate)
  • checks: percentuale di asserzioni (i check() nello script) passate con successo

Un p95 di 3 secondi con 50 utenti concorrenti, quando lo stesso endpoint risponde in 50ms con un solo utente, è un segnale chiaro: qualcosa nel sistema non scala, per esempio un pool di connessioni troppo piccolo, una query che degenera sotto concorrenza, o un lock che si allunga.

Guarda i numeri insieme, non isolati. Un p95 alto con http_req_failed a zero ti dice che il sistema rallenta ma tiene: probabilmente un collo di bottiglia su una risorsa condivisa. Un p95 basso ma con http_req_failed in crescita ti dice il contrario: il sistema smette di rispondere prima ancora di rallentare visibilmente, tipico di un pool di connessioni che si esaurisce e comincia a rifiutare richieste invece di metterle in coda.

Puoi rendere questo giudizio automatico con le thresholds: soglie che fanno fallire il test (e quindi la build, se lo integri in CI) se non vengono rispettate.

export const options = {
  vus: 50,
  duration: '30s',
  thresholds: {
    http_req_duration: ['p(95)<500'], // p95 sotto 500ms
    http_req_failed: ['rate<0.01'],   // meno dell'1% di errori
  },
};

Con questa configurazione, k6 esce con un codice di errore se il p95 supera i 500ms o se più dell'1% delle richieste fallisce. Un check di questo tipo, spostato dentro la pipeline, intercetta regressioni di performance prima che arrivino in produzione.

Uno script con vus fisso porta tutto il carico da zero a 50 utenti in un istante, cosa che raramente accade nel traffico reale. Con le stages puoi salire e scendere gradualmente:

export const options = {
  stages: [
    { duration: '10s', target: 50 },  // ramp-up
    { duration: '30s', target: 50 },  // carico costante
    { duration: '5s', target: 0 },    // ramp-down
  ],
};

Questo pattern si avvicina di più a un picco di traffico reale, dove gli utenti arrivano nel tempo invece che tutti insieme allo stesso secondo.


Errori comuni

Un paio di trappole in cui è facile cadere ai primi test:

  • Testare in produzione senza avvisare nessuno. Un load test genera traffico reale: se colpisce un ambiente condiviso, puoi degradare l’esperienza di utenti veri o far scattare alert inutili. Testa su staging, o avvisa il team prima
  • Ignorare il warm-up. Le prime richieste sono spesso più lente per motivi che non c’entrano con il tuo codice (JIT, cache fredde, connessioni non ancora stabilite). Scarta i primi secondi dall’analisi, o usa uno stage di ramp-up
  • Confondere timeout con errori applicativi. Un 5xx e un timeout raccontano storie diverse: il primo dice che il server ha risposto con un errore, il secondo che non ha risposto affatto. Distinguerli nel report ti dice se il collo di bottiglia è nel codice o nell’infrastruttura
  • Niente think time tra le richieste. Senza sleep(), ogni virtual user martella il server alla massima velocità: non è un test realistico, è un attacco DoS al tuo stesso servizio
  • Testare solo lo scenario felice. Se testi solo GET /products in condizioni ideali, non stai simulando cosa succede quando un utente cerca, filtra, o naviga tra pagine diverse nello stesso momento

Risorse utili

k6 non elimina la necessità di unit test e test di integrazione: aggiunge una dimensione che gli altri livelli di test semplicemente non coprono, quella del comportamento sotto carico concorrente. Uno script di 20 righe nella pipeline è spesso sufficiente per intercettare i problemi più costosi prima che li trovi un utente in produzione.

Non serve partire dal test più complesso che riesci a immaginare. Uno script minimo su un endpoint critico, con una soglia realistica sul p95, ti dà già un segnale che oggi probabilmente non hai: sai come si comporta il tuo sistema quando più di una persona lo usa nello stesso momento. Da lì puoi aggiungere scenari, endpoint e soglie mano a mano che capisci dove si trova il rischio reale.

Conosci meglio chi ha scritto questo articolo

Francesco Montelli

Ciao, sono Francesco. Sono un freelance software engineer con oltre 5 anni di esperienza, e mi occupo prevalentemente di sviluppo backend e DevOps aiutando i miei clienti a costruire infrastrutture solide, automatizzate e scalabili. Mi piace andare a fondo nelle cose e condividere quello che imparo. Per questo curo un blog e creo contenuti tecnici che esplorano cosa succede “sotto il cofano”, in modo chiaro e accessibile. Quando non lavoro per i clienti, sperimento nel mio homelab — un ambiente dove metto alla prova idee, strumenti e workflow prima di portarli in produzione.

Foto di Francesco Montelli

Partners

Community, aziende e persone che supportano attivamente il blog

Vuoi diventare partner?

Collaboriamo con community, aziende e strumenti del mondo tech che vogliono raggiungere la nostra community di dev ed engineer italiani. Scrivici per proporre una collaborazione.

Vuoi diventare tech content creator? 🖊️

Se ti va di raccontare la tua esperienza nel mondo tech, questo è il posto giusto.

Cerchiamo voci autentiche, esempi pratici e punti di vista utili per chi legge.

Scrivici a collaborazioni[at]theredcode.it con una proposta: idea, taglio del contenuto e una breve presentazione. Non vediamo l'ora di leggere la tua esperienza!

Invia la tua idea