· 8 min de lecture

Combien d'écritures par seconde avant qu'une nouvelle table DynamoDB ne throttle ?

AWS documente qu’une table toute neuve sert « up to 4,000 write request units per second » d’emblée. L’étude que tout le monde cite encore pour ce qui se passe vraiment — celle que Capital One et ScyllaDB référencent tous les deux — l’a mesurée en 2019, avant le warm throughput, avant les maximums configurables, avant que les règles de scaling actuelles n’existent. Autant qu’on puisse en juger, personne n’a publié de mesure depuis.

Nous en avons donc fait une. Le 2026-08-27, contre une table créée quelques minutes plus tôt dans us-east-1, avec une charge d’écriture offerte montant de 1,000 à 8,000 requêtes par seconde :

OffertAtteintRequêtes throttlées
1,000/s1,000/s0
2,000/s2,000/s0
3,000/s3,000/s0
4,000/s4,000/s0
5,000/s4,132/s25,992
6,000/s4,131/s55,966
8,000/s4,134/s115,922

La baseline documentée tient, et elle est légèrement conservatrice : le service a tout accepté jusqu’à 4,000/s sans un seul rejet, puis s’est figé à 4,130 ±2 écritures par seconde peu importe la pression exercée. Trois fenêtres, trois débits offerts, le même plafond à 0.05 % près. Les lectures n’ont jamais été throttlées : nous avons poussé une table pré-remplie au-delà de 12,700 lectures à cohérence à terme par seconde, et le manque au-dessus venait de notre propre client, pas de DynamoDB.

Une table, un jour, une région, des items d’environ 1 Ko avec des clés uniformément aléatoires — aucune en vue. Ce périmètre est la mention honnête qui accompagne chaque chiffre ici. Le reste de ce billet, c’est comment nous l’avons mesuré, y compris le passage où le benchmark a échoué trois fois avant de fonctionner, et où aucun des échecs n’était la faute de DynamoDB.

Le throttle est un 400, et il désigne d’abord le mauvais suspect

Quand le plafond est atteint, l’erreur que tu reçois mérite une lecture attentive :

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.html

Trois observations, toutes mesurées :

  • C’est un HTTP 400, pas un 5xx. Ta politique de retry et tes dashboards doivent le savoir. Un client qui ne réessaie que les 5xx les laissera tomber ; un monitoring qui n’alerte que sur les 5xx affichera un service au vert pendant qu’un tiers de tes écritures rebondit.
  • Le premier throttle est arrivé 0.9–3.8 secondes après le début de chaque fenêtre au-dessus de la baseline — le service t’accorde une courte rafale de grâce avant que le plafond ne s’engage, et plus le débit offert est élevé, plus il mord tôt.
  • L’indice de clé chaude est une valeur par défaut, pas un diagnostic. Nos clés étaient des UUID uniformément aléatoires ; il n’y avait aucune clé chaude. À petite échelle, le premier suspect pour ce message est simplement le plafond au niveau de la table.

La latence est restée indifférente à tout ça : la latence d’écriture p50 était de 4–5 ms dans la région à chaque fenêtre, throttlée ou non. Rejeter coûte peu au service : il ne ralentit pas, il dit simplement non.

Tu ne peux pas mesurer ça depuis un portable

Notre premier instrument était l’évidence même : un script Node sur un portable à Madrid. Il a cadencé 1,000 écritures par seconde proprement et s’est effondré à 2,000 — non pas parce que DynamoDB a repoussé, mais parce que ~100 ms d’aller-retour atlantique signifient que 2,000 requêtes en vol par seconde demandent des centaines de sockets concurrentes, et l’event loop s’est noyée. Le service n’a pas throttlé une seule fois. Nous benchmarkions notre propre Wi-Fi.

La deuxième tentative a déplacé le runner dans la même région, sur une unique fonction Lambda. L’aller-retour intra-région est de ~5 ms, et une seule fonction de 3 Go a cadencé 2,000 requêtes par seconde proprement à 10 ms de p50. Au-delà, elle a plafonné autour de 1,100/s avec le CPU saturé : la signature des requêtes et le traitement des réponses sont du JavaScript mono-thread, et un seul runner ne peut tout simplement pas signer 4,000 requêtes par seconde. Mémoire allouée : 3 Go. Mémoire utilisée : 253 Mo. Le goulot d’étranglement n’a jamais été la RAM — la part de CPU d’une Lambda augmente avec son réglage de mémoire, et nous achetions du calcul, pas du stockage.

L’instrument final est donc une flotte de huit Lambdas, chacune cadençant un huitième du débit offert total, toutes démarrées sur un T0 d’horloge murale partagé pour que leurs fenêtres s’alignent. Huit runners à un confortable 1,000/s chacun nous ont donné 8,000/s de charge offerte avec de la marge, et l’agrégat est une somme de requêtes comptées — aucune extrapolation nulle part.

Trois runs sont morts avant qu’un seul fonctionne, et DynamoDB était innocent à chaque fois

Le premier run de la flotte s’est terminé avec un runner rapportant qu’il avait démarré 882 secondes après T0 — quinze minutes de retard à un rendez-vous de vingt secondes. Le deuxième run est mort sur un timeout de lecture. Le troisième, avec les retries désactivés, a échoué bruyamment sur les huit runners à la fois. Pendant ce temps, CloudWatch montrait chaque Lambda terminant proprement sa mesure de quatre minutes, à l’heure, sans erreur.

Le coupable était la connexion entre le portable et Lambda. Une invocation synchrone garde une connexion HTTPS ouverte, totalement silencieuse, pendant tout le run — et un routeur résidentiel tue discrètement les connexions silencieuses au bout de quelques minutes. Le CLI, voyant une socket morte, a fait la pire chose possible : il a réessayé silencieusement, relançant une Lambda de mesure qui a alors trouvé son T0 largement dépassé. Un harnais de benchmark qui peut tourner deux fois de façon invisible n’est pas un harnais ; c’est un générateur de nombres aléatoires avec une facture AWS.

La forme qui a fini par fonctionner tient en trois règles que nous utiliserions désormais pour toute mesure distante de longue durée :

  • Tirer et oublier, résultats hors bande. Les runners sont invoqués de façon asynchrone (la connexion se ferme en millisecondes) et écrivent leurs résultats comme des items dans une petite table DynamoDB ; le driver interroge en boucle jusqu’à obtenir huit lignes de résultat. Aucune connexion ne vit plus longtemps qu’une requête.
  • Les retries sont coupés partout. Le client de mesure tourne à une tentative par requête — un retry absorberait silencieusement les throttles que nous existons pour compter — et le chemin d’invocation a lui aussi les retries désactivés, donc aucun runner ne peut jamais s’exécuter deux fois.
  • Un watchdog plutôt qu’un blocage. Chaque runner met son planning en course contre une deadline ; si quelque chose se coince, il renvoie des compteurs partiels plus un instantané de l’endroit exact où il était bloqué, au lieu d’expirer en silence. Un run raté qui s’explique coûte une lecture ; un run bloqué coûte une soirée.

Chaque requête porte aussi un timeout de 8 secondes. Le run qui s’est bloqué l’a fait parce qu’une seule requête en vol sans timeout a coincé pour toujours l’étape de drain finale. Une seule attente non bornée, pour ~4 millions de requêtes, a suffi.

Ce qu’ont fait les lectures

La phase de lecture a tourné contre une deuxième table neuve pré-remplie de 1,000 items, avec des GetItem (~1 Ko chacun, 0.5 unité de lecture) :

OffertAtteintThrottlées
4,000/s4,000/s0
8,000/s7,941/s0
12,000/s11,119/s0
16,000/s12,762/s0

Zéro throttle, jamais. La baseline documentée de 12,000 lectures/s tient et nous n’avons pas pu en trouver la limite : à 16,000/s offerts, cinq de nos huit runners ont atteint leur propre saturation côté client, donc le chiffre de 12,762/s est là où notre flotte a plafonné, pas là où DynamoDB a plafonné. Nous le disons franchement plutôt que de l’habiller en limite de service. Les lectures dans la région tournaient à 2–4 ms de p50.

Deux chiffres plus petits valent d’être retenus : une table à la demande neuve est passée de CreateTable à ACTIVE en 22 secondes lors du run de benchmark et en 7.4 secondes lors d’une sonde antérieure — prévois la variance, pas le meilleur cas. Et l’ensemble du benchmark, 672,116 écritures facturées et 1.08 million de lectures, a coûté $0.97. L’instrument est réutilisable ; l’expérience, c’est un café.

Une demi-heure de pression ne double pas le plafond

La règle de croissance d’AWS dit que la capacité à la demande absorbe jusqu’au double de ton pic précédent. Nous voulions voir ça arriver, donc après le run du plafond nous avons maintenu une table sous 8,000 écritures/s de charge offerte pendant 34 minutes d’affilée et réparti le débit atteint par tranches de 10 secondes. La forme :

Minutes sous chargePlafond
0–8~4,000/s (baseline, inchangée)
9–25~5,000/s
26~6,000/s
27–34~7,000/s
Écritures par seconde réalisées, minute par minute, sur 34 minutes soutenues à 8,000/s offertes

La croissance arrive par paliers abrupts d’environ 1,000/s, pas en rampe — une minute est plate à un débit, la minute suivante est plate au débit suivant. Le premier plafond est collant pendant 8 bonnes minutes de sur-demande continue. Et après 34 minutes la table servait 7,000/s : 1.75× son point de départ, toujours en dessous des 8,000 offerts et d’un doublement net. Si ton lancement a besoin de plus de ~4,000 écritures/s sur une table neuve, chauffe-la à l’avance ou fixe explicitement son débit maximal à la demande — le mécanisme de croissance est réel, mais il n’est ni instantané ni généreux selon ton calendrier. Le comportement est d’ailleurs stable : sous 9,000/s de charge offerte, la table de l’étude de 2019 avait grimpé à environ 7,000/s en fin de test — le même palier que la nôtre a atteint sept ans plus tard. Ce run a coûté $12.67, la chose la plus chère que nous ayons faite de la journée.

Ce qui se transpose si tu mesures un service cloud toi-même

  • Place le générateur de charge dans la même région que la cible. Sinon, tu mesures ta route, pas le service.
  • Un seul processus Node plafonne autour de 2,000 requêtes signées par seconde quelle que soit la mémoire ; répartis la charge sur plusieurs runners et somme les résultats comptés.
  • Désactive les retries sur le chemin de mesure, à chaque couche. Les retries existent pour cacher exactement ce qu’un benchmark existe pour voir.
  • Ne garde jamais une connexion silencieuse pendant un long run. Invoque en asynchrone, livre les résultats hors bande, interroge en boucle.
  • Donne un timeout à chaque requête et un watchdog à chaque runner, qui renvoie des données partielles avec un instantané de l’état bloqué.
  • Fixe un plafond dur d’opérations par runner pour qu’un bug de cadence avorte au lieu de faire grimper la facture, et démonte tout ce que le run a créé — tables, rôles, fonctions, logs — dans un finally.

Les pages de référence que ça alimente

Le jeu de données complet — chaque fenêtre, chaque runner, les percentiles de latence, les chaînes d’erreur verbatim — étaye désormais les tableaux mesurés de notre référence des limites DynamoDB, aux côtés des sondes de taille d’item et de limite de page que nous avons publiées plus tôt. Si tu travailles avec DynamoDB au quotidien, DynoTable est notre client de bureau pour ça — la même équipe, la même habitude de vérifier les affirmations contre le service réel avant de les répéter.

Travaille avec DynamoDB sans la Console

Un client de bureau rapide pour DynamoDB qui exécute le vrai SQL que DynamoDB ne peut pas — JOINs, GROUP BY, agrégations — avec édition visuelle et un agent IA sur tes propres clés Bedrock.

Essai gratuit de 30 jours, sans carte bancaire — ensuite la formule Gratuit, sans limite de durée.