¿Cuántas escrituras por segundo antes de que una tabla DynamoDB nueva haga throttling?
AWS documenta que una tabla recién creada sirve "up to 4,000 write request units per second" nada más nacer. El estudio que todo el mundo sigue citando sobre lo que pasa de verdad —el que enlazan tanto Capital One como ScyllaDB— lo midió en 2019, antes del warm throughput, antes de los máximos configurables, antes de que existieran las reglas de escalado actuales. Hasta donde sabemos, nadie ha publicado una medición desde entonces.
Así que hicimos una. El 2026-08-27, contra una tabla creada unos minutos antes en us-east-1, con carga de escritura ofrecida en rampa de 1,000 a 8,000 solicitudes por segundo:
| Ofrecidas | Conseguidas | Solicitudes con throttle |
|---|---|---|
| 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 línea base documentada se cumple, y es ligeramente conservadora: el servicio aceptó todo hasta 4,000/s sin un solo rechazo, y luego se clavó en 4,130 ±2 escrituras por segundo por mucho que apretáramos. Tres ventanas, tres tasas ofrecidas, el mismo techo con un margen del 0.05%. Las lecturas no sufrieron throttling en ningún momento — llevamos una tabla precargada más allá de 12,700 lecturas con consistencia eventual por segundo, y lo que faltaba por encima de esa cifra era nuestro propio cliente, no DynamoDB.
Una tabla, un día, una región, Items de ~1 KB con claves uniformemente aleatorias — ninguna a la vista. Ese alcance es la letra pequeña honesta de cada cifra de aquí. El resto de este artículo es cómo lo medimos, incluida la parte en la que el benchmark falló tres veces antes de funcionar, y ninguno de los fallos fue culpa de DynamoDB.
El throttle es un 400, y señala primero al sospechoso equivocado
Cuando llegas al techo, el error que recibes merece una lectura atenta:
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.htmlTres observaciones, todas medidas:
- Es un HTTP 400, no un 5xx. Tu política de reintentos y tus paneles necesitan saberlo. Un cliente que solo reintenta los 5xx dejará caer estos al suelo; un monitor que solo alerta con 5xx mostrará un servicio en verde mientras un tercio de tus escrituras rebota.
- El primer throttle llegó entre 0.9 y 3.8 segundos después de empezar cada ventana por encima de la línea base — el servicio te concede una ráfaga de gracia corta antes de que el techo entre en juego, y cuanto más alta es la tasa ofrecida, antes muerde.
- La pista sobre la clave caliente es un valor por defecto, no un diagnóstico. Nuestras claves eran UUID uniformemente aleatorios; no había ninguna clave caliente. A pequeña escala, el primer sospechoso de este mensaje es sencillamente el techo a nivel de tabla.
La latencia se mantuvo indiferente a todo ello: la latencia p50 de escritura fue de 4–5 ms dentro de la región en todas las ventanas, con throttling o sin él. Rechazar le sale barato al servicio — no se ralentiza, simplemente dice que no.
Esto no se puede medir desde un portátil
Nuestro primer instrumento fue el obvio: un script Node en un portátil en Madrid. Marcó 1,000 escrituras por segundo con limpieza y se hundió a 2,000 — no porque DynamoDB se resistiera, sino porque ~100 ms de ida y vuelta atlántica significan que 2,000 solicitudes en vuelo por segundo necesitan cientos de sockets concurrentes, y el bucle de eventos se ahogó. El servicio no hizo throttling ni una sola vez. Estábamos midiendo nuestro propio Wi-Fi.
El segundo intento movió el runner a la misma región, como una única función Lambda. La ida y vuelta dentro de la región es de ~5 ms, y una sola función de 3 GB marcó 2,000 solicitudes por segundo con limpieza a 10 ms de p50. Por encima de eso se quedó plana en torno a 1,100/s con la CPU al máximo: firmar solicitudes y manejar respuestas es JavaScript de un solo hilo, y un runner sencillamente no puede firmar 4,000 solicitudes por segundo. Memoria asignada: 3 GB. Memoria usada: 253 MB. El cuello de botella nunca fue la RAM — la porción de CPU de una Lambda escala con su ajuste de memoria, y lo que comprábamos era cómputo, no almacenamiento.
Así que el instrumento final es una flota de ocho Lambdas, cada una marcando un octavo de la tasa ofrecida total, todas arrancadas contra un T0 de reloj compartido para que sus ventanas cuadren. Ocho runners a unos cómodos 1,000/s cada uno nos dieron 8,000/s de carga ofrecida con margen, y el agregado es una suma de solicitudes contadas — sin extrapolación en ninguna parte.
Tres ejecuciones murieron antes de que una funcionara, y DynamoDB era inocente en todas
La primera ejecución de la flota terminó con un runner informando de que había arrancado 882 segundos después de T0 — quince minutos tarde a una cita de veinte segundos. La segunda ejecución murió con un timeout de lectura. La tercera, con los reintentos desactivados, falló a gritos en los ocho runners a la vez. Mientras tanto, CloudWatch mostraba cada una de las Lambdas terminando su medición de cuatro minutos con limpieza, a tiempo y sin errores.
El culpable era la conexión entre el portátil y Lambda. Una invocación síncrona mantiene abierta una conexión HTTPS, completamente muda, durante toda la ejecución — y un router doméstico mata en silencio las conexiones mudas al cabo de unos minutos. La CLI, al ver un socket muerto, hizo lo peor posible: reintentó en silencio, volviendo a lanzar una Lambda de medición que se encontró con su T0 ya pasado hacía rato. Un arnés de benchmark que puede ejecutarse dos veces de forma invisible no es un arnés; es un generador de números aleatorios con una factura de AWS.
La forma que acabó funcionando tiene tres reglas que ahora usaríamos para cualquier medición remota de larga duración:
- Dispara y olvida, resultados fuera de banda. Los runners se invocan de forma asíncrona (la conexión se cierra en milisegundos) y escriben sus resultados como Items en una tabla pequeña de DynamoDB; el driver sondea hasta ver ocho filas de resultado. Ninguna conexión vive más que una solicitud.
- Los reintentos están desactivados en todas partes. El cliente de medición corre con un solo intento por solicitud —un reintento absorbería en silencio los throttles que existimos para contar— y la ruta de invocación también tiene los reintentos desactivados, así que ningún runner puede ejecutarse dos veces.
- Un watchdog en vez de un cuelgue. Cada runner corre su calendario contra un plazo límite; si algo se atasca, devuelve los recuentos parciales más una instantánea de exactamente dónde se quedó, en lugar de expirar en silencio. Una ejecución fallida que se explica cuesta una lectura; una colgada cuesta una tarde.
Cada solicitud lleva además un timeout de 8 segundos. La ejecución que se colgó lo hizo porque una única solicitud en vuelo sin timeout atascó para siempre el paso final de vaciado. Una sola espera sin límite, cada ~4 millones de solicitudes, bastó.
Qué hicieron las lecturas
La fase de lectura corrió contra una segunda tabla nueva precargada con 1,000 Items, usando GetItem (~1 KB cada uno, 0.5 unidades de lectura):
| Ofrecidas | Conseguidas | Con throttle |
|---|---|---|
| 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 |
Cero throttles, nunca. La línea base documentada de 12,000 lecturas/s se cumple y no pudimos encontrar su borde: con 16,000/s ofrecidas, cinco de nuestros ocho runners alcanzaron su propia saturación en el cliente, así que la cifra de 12,762/s es donde topó nuestra flota, no donde topó DynamoDB. Lo decimos sin rodeos en vez de disfrazarlo de límite del servicio. Las lecturas dentro de la región corrieron a 2–4 ms de p50.
Dos cifras más pequeñas que conviene guardar: una tabla bajo demanda nueva pasó de CreateTable a ACTIVE en 22 segundos en la ejecución del benchmark y en 7.4 segundos en una sonda anterior — presupuesta la varianza, no el mejor caso. Y el benchmark entero, 672,116 escrituras facturadas y 1.08 millones de lecturas, costó $0.97. El instrumento es reutilizable; el experimento es un café.
Media hora de presión no duplica el techo
La regla de crecimiento de AWS dice que la capacidad bajo demanda admite hasta el doble de tu pico anterior. Queríamos verlo ocurrir, así que después de la ejecución del techo mantuvimos una tabla a 8,000 escrituras/s de carga ofrecida durante 34 minutos seguidos y agrupamos la tasa conseguida en tramos de 10 segundos. La forma:
| Minutos bajo carga | Techo |
|---|---|
| 0–8 | ~4,000/s (línea base, sin moverse) |
| 9–25 | ~5,000/s |
| 26 | ~6,000/s |
| 27–34 | ~7,000/s |
El crecimiento llega en escalones abruptos de ~1,000/s, no en rampa: un minuto es plano a una tasa, el minuto siguiente es plano a la siguiente. El primer techo se resiste durante 8 minutos enteros de sobredemanda continua. Y tras 34 minutos la tabla servía 7,000/s: 1.75× de donde partió, todavía por debajo de los 8,000 ofrecidos y de una duplicación limpia. Si tu lanzamiento necesita más de ~4,000 escrituras/s en una tabla nueva, caliéntala con antelación o fija su rendimiento máximo bajo demanda de forma explícita — el mecanismo de crecimiento es real, pero no es ni instantáneo ni generoso con tu calendario. El comportamiento, además, es estable: bajo 9,000/s de carga ofrecida, la tabla del estudio de 2019 había crecido hasta unas 7,000/s al terminar la prueba — la misma meseta que la nuestra alcanzó siete años después. Esa ejecución costó $12.67, lo más caro que hicimos en todo el día.
Qué se transfiere si mides un servicio en la nube por tu cuenta
- Pon el generador de carga en la misma región que el objetivo. Si no, estás midiendo tu ruta, no el servicio.
- Un solo proceso Node topa en torno a 2,000 solicitudes firmadas por segundo, sea cual sea su memoria; reparte la carga entre runners y suma resultados contados.
- Desactiva los reintentos en la ruta de medición, en todas las capas. Los reintentos existen para ocultar exactamente lo que un benchmark existe para ver.
- Nunca mantengas una conexión muda durante una ejecución larga. Invoca en asíncrono, entrega los resultados fuera de banda, sondea.
- Dale a cada solicitud un timeout y a cada runner un watchdog que devuelva datos parciales con una instantánea del estado atascado.
- Fija un techo duro de operaciones por runner para que un bug de ritmo aborte en lugar de disparar la factura, y desmonta todo lo que la ejecución creó —tablas, roles, funciones, logs— en un
finally.
Las páginas de referencia que esto alimenta
El conjunto de datos completo —cada ventana, cada runner, los percentiles de latencia, las cadenas de error literales— respalda ahora las tablas medidas de nuestra referencia de límites de DynamoDB, junto a las sondas de tamaño de Item y de límite de página que publicamos antes. Si trabajas con DynamoDB a diario, DynoTable es nuestro cliente de escritorio para ello — el mismo equipo, la misma costumbre de comprobar las afirmaciones contra el servicio en vivo antes de repetirlas.