Quante scritture al secondo prima che una nuova tabella DynamoDB vada in throttling?
AWS documenta che una tabella appena creata serve "up to 4,000 write request units per second" già di suo. Lo studio che tutti citano ancora per sapere cosa succede davvero — quello che linkano sia Capital One sia ScyllaDB — l'ha misurato nel 2019, prima del warm throughput, prima dei massimi configurabili, prima che esistessero le regole di scaling attuali. Per quanto ne sappiamo, da allora nessuno ha pubblicato una misurazione.
Così ne abbiamo fatta una. Il 2026-08-27, contro una tabella creata pochi minuti prima in us-east-1, con carico di scrittura offerto in rampa da 1,000 a 8,000 richieste al secondo:
| Offerto | Raggiunto | Richieste in throttling |
|---|---|---|
| 1,000/s | 1,000/s | 0 |
| 2,000/s | 2,000/s | 0 |
| 3,000/s | 3,000/s | 0 |
| 4,000/s | 4,000/s | 0 |
| 5,000/s | 4,132/s | 25,992 |
| 6,000/s | 4,131/s | 55,966 |
| 8,000/s | 4,134/s | 115,922 |
La baseline documentata regge, ed è leggermente conservativa: il servizio ha accettato tutto fino a 4,000/s senza un singolo rifiuto, poi si è fermato a 4,130 ±2 scritture al secondo per quanto forte spingessimo. Tre finestre, tre tassi offerti, lo stesso tetto entro lo 0.05%. Le letture non sono mai andate in throttling — abbiamo spinto una tabella popolata oltre le 12,700 letture eventualmente coerenti al secondo e il divario oltre quella soglia era il nostro client, non DynamoDB.
Una tabella, un giorno, una regione, item da ~1 KB con chiavi uniformemente casuali — nessuna in vista. Quel perimetro è la scritta in piccolo, onesta, dietro ogni numero qui. Il resto di questo post è come l'abbiamo misurato, compresa la parte in cui il benchmark è fallito tre volte prima di funzionare, e nessuno dei fallimenti era colpa di DynamoDB.
Il throttling è un 400, e nomina per primo il sospettato sbagliato
Quando il tetto scatta, vale la pena leggere con attenzione l'errore che ricevi:
ThrottlingException: Throughput exceeds the current capacity of your table or index.
DynamoDB is automatically scaling your table or index so please try again shortly.
If exceptions persist, check if you have a hot key:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.htmlTre osservazioni, tutte misurate:
- È un HTTP 400, non un 5xx. La tua policy di retry e le tue dashboard devono saperlo. Un client che ritenta solo i 5xx li lascerà cadere a terra; un monitor che allarma solo sui 5xx mostrerà un servizio verde mentre un terzo delle tue scritture rimbalza.
- Il primo throttling è arrivato tra 0.9 e 3.8 secondi dopo l'inizio di ogni finestra sopra la baseline — il servizio ti concede un breve burst di grazia prima che il tetto entri in funzione, e più alto è il tasso offerto, prima morde.
- Il suggerimento sulla hot key è un default, non una diagnosi. Le nostre chiavi erano UUID uniformemente casuali; non c'era nessuna hot key. Su piccola scala, il primo sospettato per questo messaggio è semplicemente il tetto a livello di tabella.
La latenza è rimasta indifferente a tutto questo: la latenza di scrittura p50 è stata di 4–5 ms in-region in ogni finestra, con o senza throttling. Rifiutare costa poco al servizio — non rallenta, dice solo no.
Non puoi misurarlo da un portatile
Il nostro primo strumento era quello ovvio: uno script Node su un portatile a Madrid. Ha scandito 1,000 scritture al secondo in modo pulito ed è collassato a 2,000 — non perché DynamoDB abbia opposto resistenza, ma perché ~100 ms di round-trip atlantico significano che 2,000 richieste in volo al secondo richiedono centinaia di socket concorrenti, e l'event loop è annegato. Il servizio non è mai andato in throttling nemmeno una volta. Stavamo facendo il benchmark del nostro Wi-Fi.
Il secondo tentativo ha spostato il runner nella stessa regione, dentro una singola funzione Lambda. Il round-trip in-region è di ~5 ms, e una sola funzione da 3 GB ha scandito 2,000 richieste al secondo in modo pulito con 10 ms di p50. Oltre quella soglia si è appiattita intorno a 1,100/s con la CPU al massimo: la firma delle richieste e la gestione delle risposte sono JavaScript a thread singolo, e un solo runner non può semplicemente firmare 4,000 richieste al secondo. Memoria allocata: 3 GB. Memoria usata: 253 MB. Il collo di bottiglia non è mai stata la RAM — la quota di CPU di una Lambda cresce con la sua impostazione di memoria, e noi stavamo comprando calcolo, non spazio.
Quindi lo strumento finale è una flotta di otto Lambda, ognuna delle quali scandisce un ottavo del tasso offerto totale, tutte avviate contro un T0 condiviso a orologio di parete perché le loro finestre si allineino. Otto runner a un comodo 1,000/s ciascuno ci hanno dato 8,000/s di carico offerto con margine, e l'aggregato è una somma di richieste contate — nessuna estrapolazione da nessuna parte.
Tre run sono morti prima che uno funzionasse, e DynamoDB era innocente ogni volta
Il primo run della flotta è finito con un runner che riportava di essere partito 882 secondi dopo T0 — quindici minuti di ritardo a un appuntamento di venti secondi. Il secondo run è morto con un read timeout. Il terzo, con i retry disabilitati, è fallito rumorosamente su tutti e otto i runner insieme. Nel frattempo CloudWatch mostrava ogni singola Lambda che finiva la sua misurazione di quattro minuti in modo pulito, in orario, senza errori.
Il colpevole era la connessione tra il portatile e Lambda. Un'invocazione sincrona tiene aperta una connessione HTTPS, completamente silenziosa, per tutta la durata del run — e un router domestico uccide silenziosamente le connessioni silenziose dopo qualche minuto. La CLI, vedendo un socket morto, ha fatto la cosa peggiore possibile: ha ritentato in silenzio, rieseguendo una Lambda di misurazione che poi ha trovato il proprio T0 ormai passato da un pezzo. Un'infrastruttura di benchmark che può girare due volte senza che tu lo veda non è un'infrastruttura; è un generatore di numeri casuali con una bolletta AWS.
La forma che alla fine ha funzionato ha tre regole che ora useremmo per qualsiasi misurazione remota di lunga durata:
- Spara e dimentica, risultati fuori banda. I runner vengono invocati in modo asincrono (la connessione si chiude in millisecondi) e scrivono i propri risultati come item in una piccola tabella DynamoDB; il driver interroga finché non trova otto righe di risultati. Nessuna connessione vive più di una richiesta.
- I retry sono disattivati ovunque. Il client di misurazione lavora con un solo tentativo per richiesta — un retry assorbirebbe in silenzio proprio i throttling che esistiamo per contare — e anche il percorso di invocazione ha i retry disabilitati, così nessun runner può mai eseguire due volte.
- Un watchdog invece di un blocco. Ogni runner mette la propria pianificazione in gara con una deadline; se qualcosa si incastra, restituisce conteggi parziali più un'istantanea di dove esattamente era bloccato, invece di andare in timeout in silenzio. Un run fallito che si spiega costa una lettura; uno bloccato costa una serata.
Ogni richiesta porta anche un timeout di 8 secondi. Il run che si è bloccato l'ha fatto perché una singola richiesta in volo senza timeout ha incastrato per sempre il passo finale di svuotamento. Una sola attesa senza limite, ogni ~4 milioni di richieste, è stata sufficiente.
Cosa hanno fatto le letture
La fase di lettura è girata contro una seconda tabella nuova popolata con 1,000 item, usando GetItem (~1 KB ciascuno, 0.5 unità di lettura):
| Offerto | Raggiunto | In throttling |
|---|---|---|
| 4,000/s | 4,000/s | 0 |
| 8,000/s | 7,941/s | 0 |
| 12,000/s | 11,119/s | 0 |
| 16,000/s | 12,762/s | 0 |
Zero throttling, mai. La baseline documentata di 12,000 letture/s regge e non siamo riusciti a trovarne il limite: a 16,000/s offerti, cinque degli otto runner hanno raggiunto la propria saturazione lato client, quindi il valore di 12,762/s è dove si è fermata la nostra flotta, non dove si è fermato DynamoDB. Lo diciamo chiaramente invece di vestirlo da limite del servizio. Le letture in-region sono girate a 2–4 ms di p50.
Due numeri più piccoli che vale la pena tenere: una tabella on-demand nuova è passata da CreateTable ad ACTIVE in 22 secondi nel run del benchmark e in 7.4 secondi in una prova precedente — metti a budget la varianza, non il caso migliore. E l'intero benchmark, 672,116 scritture fatturate e 1.08 milioni di letture, è costato $0.97. Lo strumento è riutilizzabile; l'esperimento è un caffè.
Mezz'ora di pressione non raddoppia il tetto
La regola di crescita di AWS dice che la capacità on-demand accoglie fino al doppio del tuo picco precedente. Volevamo vederlo accadere, quindi dopo il run sul tetto abbiamo tenuto una tabella a 8,000 scritture/s di carico offerto per 34 minuti contigui e abbiamo raggruppato il tasso raggiunto ogni 10 secondi. La forma:
| Minuti sotto carico | Tetto |
|---|---|
| 0–8 | ~4,000/s (baseline, invariata) |
| 9–25 | ~5,000/s |
| 26 | ~6,000/s |
| 27–34 | ~7,000/s |
La crescita arriva a gradini bruschi di ~1,000/s, non in rampa — un minuto è piatto a un tasso, il minuto dopo è piatto al tasso successivo. Il primo tetto resta appiccicato per 8 minuti pieni di sovraccarico continuo. E dopo 34 minuti la tabella serviva 7,000/s: 1.75× il punto di partenza, ancora sotto agli 8,000 offerti e a un raddoppio pulito. Se il tuo lancio ha bisogno di più di ~4,000 scritture/s su una tabella nuova, scaldala in anticipo oppure imposta esplicitamente il suo throughput on-demand massimo — il meccanismo di crescita è reale ma non è né istantaneo né generoso secondo i tuoi tempi. Il comportamento è anche stabile: sotto 9,000/s di carico offerto, la tabella dello studio del 2019 era cresciuta fino a circa 7,000/s a fine test — lo stesso plateau che la nostra ha raggiunto sette anni dopo. Quel run è costato $12.67, la cosa più cara che abbiamo fatto in tutto il giorno.
Cosa si trasferisce se misuri da solo un servizio cloud
- Metti il generatore di carico nella stessa regione del bersaglio. Altrimenti stai misurando il tuo percorso di rete, non il servizio.
- Un solo processo Node si ferma intorno a 2,000 richieste firmate al secondo indipendentemente dalla memoria; distribuisci il carico su più runner e somma i risultati contati.
- Disabilita i retry sul percorso di misurazione, a ogni livello. I retry esistono per nascondere esattamente ciò che un benchmark esiste per vedere.
- Non tenere mai una connessione silenziosa per tutta la durata di un run lungo. Invoca in modo asincrono, consegna i risultati fuori banda, interroga.
- Dai a ogni richiesta un timeout e a ogni runner un watchdog che restituisca dati parziali con un'istantanea dello stato in cui si è bloccato.
- Imposta un tetto rigido di operazioni per runner, così un bug di scansione aborta invece di far salire la bolletta, e smonta tutto ciò che il run ha creato — tabelle, ruoli, funzioni, log — in un
finally.
Le pagine di riferimento che questo alimenta
Il dataset completo — ogni finestra, ogni runner, i percentili di latenza, le stringhe di errore verbatim — sta ora dietro alle tabelle misurate nel nostro riferimento sui limiti di DynamoDB, insieme alle prove su dimensione degli item e limiti di pagina che abbiamo pubblicato prima. Se lavori con DynamoDB ogni giorno, DynoTable è il nostro client desktop per farlo — stesso team, stessa abitudine di verificare le affermazioni contro il servizio vivo prima di ripeterle.