SaaS a 10.000 utenti: i 6 problemi di scalabilità che arrivano prima

Diecimila utenti registrati sono un carico piccolo. Il calcolo qui sotto li porta a una ventina di richieste in corso nell'ora di punta. A questa scala cedono scelte che a 500 utenti non costavano niente: una connessione al database aperta a ogni richiesta, una tabella letta senza indice, un'email inviata mentre l'utente aspetta. Sono i problemi di scalabilità di un SaaS che arrivano prima, e ognuno costa meno risolverlo prima che arrivi il traffico.
Perché 10.000 utenti mettono in difficoltà un SaaS?
Perché 10.000 utenti non sono 10.000 persone collegate insieme. La legge di Little, un risultato della teoria delle code, dice che le richieste in corso sono uguali al tasso di arrivo moltiplicato per la durata di ogni richiesta. Facciamo il conto per un prodotto B2B. Ipotizziamo che nell'ora di punta sia attivo il 10% degli utenti e che ognuno generi una richiesta ogni 10 secondi. Sono 1.000 persone e 100 richieste al secondo; a 200 millisecondi per richiesta, quelle in corso sono 20. Un server modesto le gestisce senza sforzo.
Il carico, quindi, è piccolo. I guasti nascono da quanto spende ogni richiesta: connessioni, righe lette, secondi di lavoro, chiamate ad altri servizi. Un'architettura che spende un po' troppo per richiesta funziona a 500 utenti e si ferma a 10.000. Ecco i sei punti in cui succede.
1. Il database esaurisce le connessioni
PostgreSQL imposta di default max_connections a 100, e ogni connessione è un processo del server con la sua memoria (documentazione di PostgreSQL). I piani gestiti fissano un tetto proprio. Su Supabase il compute Micro accetta 60 connessioni dirette e 200 attraverso il pooler, e il limite del pooler sale solo con un compute più grande (Supabase).
Sull'hosting serverless ogni istanza di funzione attiva nello stesso momento può aprire la sua connessione. Un picco di traffico, un deploy che riavvia a freddo tutte le istanze o uno script che inserisce righe in un ciclo bastano a esaurirle. Il sintomo è una raffica di errori too many connections con la CPU ferma.
La soluzione: il traffico dell'applicazione passa da un pooler in modalità transaction, le connessioni dirette restano per le migrazioni e le sessioni lunghe, e ogni istanza ha un tetto alle connessioni che può tenere aperte. La guida di Supabase sulla gestione delle connessioni consiglia la stessa divisione. Quando il pooler aiuta e quando no l'abbiamo spiegato in Supabase Supavisor: quando il pooler ti salva.
2. Query veloci a 1.000 righe, lente a un milione
Una query senza indice legge tutta la tabella. A 1.000 righe nessuno se ne accorge. Con 10.000 utenti che scrivono eventi, fatture e commenti, le stesse tabelle contengono milioni di righe, e una pagina che si apriva subito comincia a metterci secondi.
Due casi vanno controllati per primi. Il primo sono le chiavi esterne: PostgreSQL indicizza la colonna referenziata ma, per scelta, non crea un indice sulle colonne che la referenziano. Ogni colonna project_id, tenant_id e user_id ha bisogno del suo indice, scritto nella migrazione. Il secondo è la paginazione con OFFSET. Secondo la documentazione, le righe saltate da un OFFSET il server le calcola comunque, quindi la pagina 500 costa il lavoro di 500 pagine. La paginazione per chiave (WHERE created_at < $ultimo) costa uguale a ogni pagina.
La row-level security aggiunge un terzo caso: una policy che chiama una funzione per ogni riga trasforma una query in migliaia di chiamate. I pattern che la tengono veloce sono in Supabase RLS in scala.
3. Il lavoro lento gira dentro la richiesta
Inviare l'email di benvenuto, generare la fattura in PDF, chiamare un modello AI, notificare un webhook: ognuna di queste operazioni dura da mezzo secondo a mezzo minuto e dipende da un servizio che il prodotto non controlla. Dentro la richiesta, ogni secondo di quel lavoro occupa un'istanza di funzione, spesso anche una connessione al database, e l'utente aspetta.
Quando il servizio esterno rallenta, le richieste si accumulano in fila. Di nuovo la legge di Little: se raddoppia la durata di ogni richiesta, raddoppiano anche quelle in corso, senza un utente in più.
La soluzione è una coda. La richiesta registra il lavoro e risponde in pochi millisecondi; un worker lo esegue, riprova se fallisce e salva il risultato. Due regole la rendono sicura. Ogni job è idempotente, così un nuovo tentativo non manda due fatture. Ogni job ha un timeout e un numero massimo di tentativi, così un input sbagliato non gira all'infinito. Una coda su Redis è una delle strade; i pattern che usiamo sono nei 5 pattern di Upstash Redis.
4. Un cliente rallenta tutti gli altri
In un prodotto multi-tenant i clienti condividono lo stesso database e gli stessi worker. Le linee guida di architettura di Microsoft lo chiamano problema del vicino rumoroso (noisy neighbor): l'attività di un tenant peggiora il servizio per gli altri, che vedono richieste fallite o lente. A 10.000 utenti la distanza tra l'account più piccolo e il più grande basta perché succeda. Un cliente importa 200.000 righe il lunedì mattina e la dashboard rallenta per tutti.
La soluzione è trattare il tenant come un'unità da misurare e limitare. Ogni query, job e riga di log porta l'id del tenant. Le operazioni pesanti (import, export, modifiche in blocco) passano da una coda per tenant con la sua concorrenza, e le quote dipendono dal piano. Quando un account diventa troppo grande per l'infrastruttura condivisa, lo si sposta su un database o su worker dedicati. Lo spostamento è molto più semplice se il tenant è nello schema dall'inizio, la tesi di multi-tenant dal primo giorno.
5. Nessun limite a quello che un client può chiedere
OWASP mette il consumo di risorse senza limiti al quarto posto della API Security Top 10 del 2023 ed elenca i limiti che mancano a un'API vulnerabile: timeout di esecuzione, memoria, record per pagina, tetti di spesa sui fornitori esterni. Un prodotto con 10.000 utenti ha script, integrazioni e qualche account che abusa. Un endpoint che restituisce tutti i record, o che chiama un modello AI a pagamento senza tetto per utente, trasforma uno di questi in un disservizio o in una fattura.
La soluzione: rate limit per utente e per chiave API, una dimensione massima di pagina imposta dal server, un timeout su ogni chiamata in uscita, e un tetto di spesa o un avviso di fatturazione su ogni fornitore a pagamento. Sono poche righe di codice ciascuno.
6. I job in background leggono tutto
Il job notturno che ricalcola l'utilizzo, il report che somma il fatturato, la pulizia che cancella le sessioni scadute: scritti al lancio, leggono ognuno l'intera tabella. A 10.000 utenti il giro notturno dura ore, si sovrappone a quello successivo e tiene i lock durante il primo traffico del mattino.
La soluzione è lavorare solo su ciò che è cambiato. Si tiene un segnaposto (l'ultimo id o timestamp elaborato), si procede a blocchi di qualche migliaio di righe con un commit dopo ognuno, e le letture pesanti vanno su una replica in lettura, se il piano la prevede. I contatori si aggiornano quando arrivano gli eventi, invece di ricontare da zero ogni notte.
Come si vedono questi problemi prima degli utenti?
Tutti e sei compaiono nei numeri prima che nei ticket di assistenza. Ne bastano quattro da tenere d'occhio:
- Connessioni aperte rispetto al limite, con un avviso al 70%.
- Le query più lente per tempo totale, da
pg_stat_statements, riviste ogni settimana. - La latenza p95 per endpoint, non la media, che nasconde le richieste più lente.
- Lunghezza della coda ed età dei job, così un worker bloccato si vede in pochi minuti.
Poi si prova alla dimensione che il prodotto prevede di raggiungere. Si riempie un database di staging con dieci volte i dati di oggi, si ripetono gli endpoint più usati con uno strumento di carico come k6 e si leggono i piani delle query con EXPLAIN ANALYZE. Un pomeriggio di prove mostra quali dei sei problemi ha il prodotto.
Cosa sistemare per primo
L'ordine lo decide il costo del guasto, non l'eleganza. Limiti di connessione e indici mancanti fermano tutto il prodotto e si sistemano in poche ore: vengono per primi. Code e limiti per tenant richiedono giorni e proteggono il prodotto dai clienti più grandi. I job notturni a blocchi possono aspettare finché non durano più di un'ora. Nessuno dei sei richiede microservizi, Kubernetes o una riscrittura. Un solo database Postgres e un'applicazione fatta bene portano un SaaS molto oltre i 10.000 utenti, se ogni richiesta spende solo quello che le serve.
Domande frequenti
Molti meno di 10.000. Il numero lo dà la legge di Little: le richieste in corso sono le richieste al secondo moltiplicate per la durata media di ognuna. Se nell'ora di punta è attivo il 10% degli utenti e ognuno genera una richiesta ogni 10 secondi, sono 100 richieste al secondo e, a 200 millisecondi ciascuna, circa 20 in corso. Conviene misurare l'ora di punta negli analytics, non il totale giornaliero: i prodotti usati al lavoro concentrano il traffico in orario d'ufficio.
No. Tutti e sei i problemi di questo articolo si risolvono dentro un'applicazione e un database Postgres: un pooler, gli indici giusti, una coda, limiti per tenant, rate limit e job a blocchi. Dividere in servizi aggiunge chiamate di rete, deploy separati e più punti in cui qualcosa può fallire. Conviene quando una parte del prodotto ha un carico diverso dal resto, come l'elaborazione video, o quando la gestisce un team separato.
Dopo le correzioni, non prima. Un piano più grande alza il limite di connessioni e aggiunge CPU, ma il costo di un indice mancante cresce con la tabella, quindi l'upgrade fatto per primo rimanda soltanto il problema. Si guarda pg_stat_statements: quando le query più lente usano già gli indici e la CPU resta alta nell'ora di punta, si passa a un compute più grande o si aggiunge una replica in lettura per report ed export.
Sì, e contano più i dati del traffico. Si riempie un database di staging con dieci volte le righe di oggi, perché i piani delle query cambiano con la dimensione delle tabelle, poi si ripetono gli endpoint più usati con uno strumento come k6. Il test va fatto sullo stesso piano di database della produzione, perché i limiti di connessione cambiano con il compute. Dopo si leggono le query più lente con EXPLAIN ANALYZE: è lì che compaiono quasi tutti i sei problemi.
Articoli correlati
29 ottobre 2026 · 10 min di lettura
6 luglio 2026 · 7 min di lettura
18 aprile 2026 · 7 min di lettura