Quantas gravações por segundo antes de uma tabela DynamoDB nova sofrer throttling?
A AWS documenta que uma tabela recém-criada entrega "up to 4,000 write request units per second" já de saída. O estudo que todo mundo ainda cita para saber o que acontece de verdade — aquele que a Capital One e a ScyllaDB linkam — mediu isso em 2019, antes do warm throughput, antes dos máximos configuráveis, antes das regras de escala atuais existirem. Até onde conseguimos ver, ninguém publicou uma medição desde então.
Então nós rodamos uma. Em 2026-08-27, contra uma tabela criada minutos antes em us-east-1, com carga de gravação oferecida subindo de 1,000 para 8,000 requisições por segundo:
| Oferecido | Alcançado | Requisições com 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 |
A linha de base documentada se sustenta, e ela é levemente conservadora: o serviço aceitou tudo até 4,000/s sem uma única rejeição, depois travou em 4,130 ±2 gravações por segundo por mais que a gente empurrasse. Três janelas, três taxas oferecidas, o mesmo teto com margem de 0.05%. As leituras nunca sofreram throttling — levamos uma tabela populada a mais de 12,700 leituras eventualmente consistentes por segundo e a diferença acima disso era do nosso próprio cliente, não do DynamoDB.
Uma tabela, um dia, uma região, itens de ~1 KB com chaves uniformemente aleatórias — nenhuma à vista. Esse escopo é a letra miúda honesta de cada número aqui. O resto deste post é como medimos, incluindo a parte em que o benchmark falhou três vezes antes de funcionar, e nenhuma das falhas foi culpa do DynamoDB.
O throttle é um 400, e ele aponta o suspeito errado primeiro
Quando o teto é atingido, o erro que você recebe merece uma leitura 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.htmlTrês observações, todas medidas:
- É um HTTP 400, não um 5xx. Sua política de retry e seus dashboards precisam saber disso. Um cliente que só refaz 5xx vai jogar essas requisições no chão; um monitor que só alarma em 5xx vai mostrar um serviço verde enquanto um terço das suas gravações quica.
- O primeiro throttle chegou de 0.9 a 3.8 segundos após o início de cada janela acima da linha de base — o serviço te dá uma curta rajada de tolerância antes de o teto entrar, e quanto maior a taxa oferecida, mais cedo ele morde.
- A dica de hot key é um padrão, não um diagnóstico. Nossas chaves eram UUIDs uniformemente aleatórios; não havia hot key. Em pequena escala, o primeiro suspeito para essa mensagem é simplesmente o teto no nível da tabela.
A latência ficou indiferente a tudo isso: a latência p50 de gravação foi de 4 a 5 ms dentro da região em todas as janelas, com ou sem throttling. Rejeitar é barato para o serviço — ele não desacelera, apenas diz não.
Você não consegue medir isso de um laptop
Nosso primeiro instrumento foi o óbvio: um script Node em um laptop em Madri. Ele manteve o ritmo de 1,000 gravações por segundo com folga e desabou em 2,000 — não porque o DynamoDB empurrou de volta, mas porque ~100 ms de ida e volta pelo Atlântico significam que 2,000 requisições em voo por segundo exigem centenas de sockets simultâneos, e o event loop se afogou. O serviço nunca aplicou throttling nenhuma vez. Estávamos medindo o nosso próprio Wi-Fi.
A segunda tentativa moveu o executor para a mesma região, em uma única função Lambda. A ida e volta dentro da região é de ~5 ms, e uma função de 3 GB manteve 2,000 requisições por segundo com folga a 10 ms de p50. Passando disso, ela estabilizou em torno de 1,100/s com a CPU no limite: a assinatura das requisições e o tratamento das respostas são JavaScript de thread única, e um executor simplesmente não consegue assinar 4,000 requisições por segundo. Memória alocada: 3 GB. Memória usada: 253 MB. O gargalo nunca foi RAM — a fatia de CPU de uma Lambda escala com a configuração de memória, e estávamos comprando processamento, não armazenamento.
Então o instrumento final é uma frota de oito Lambdas, cada uma mantendo um oitavo da taxa total oferecida, todas iniciadas contra um T0 de relógio compartilhado para que suas janelas se alinhem. Oito executores a confortáveis 1,000/s cada nos deram 8,000/s de carga oferecida com folga, e o agregado é uma soma de requisições contadas — nenhuma extrapolação em lugar nenhum.
Três execuções morreram antes de uma funcionar, e o DynamoDB era inocente todas as vezes
A primeira execução da frota terminou com um executor relatando que havia começado 882 segundos após o T0 — quinze minutos atrasado para um compromisso de vinte segundos. A segunda execução morreu com um timeout de leitura. A terceira, com os retries desligados, falhou ruidosamente nos oito executores de uma vez. Enquanto isso, o CloudWatch mostrava cada uma das Lambdas terminando seus quatro minutos de medição sem problemas, no horário, sem erros.
O culpado era a conexão entre o laptop e a Lambda. Uma invocação síncrona mantém uma conexão HTTPS aberta, totalmente silenciosa, durante toda a execução — e um roteador residencial mata em silêncio conexões silenciosas depois de alguns minutos. A CLI, vendo um socket morto, fez a pior coisa possível: ela refez a chamada em silêncio, reexecutando uma Lambda de medição que então encontrava seu T0 já muito distante. Um arcabouço de benchmark que pode rodar duas vezes de forma invisível não é um arcabouço; é um gerador de números aleatórios com uma fatura da AWS.
O formato que finalmente funcionou tem três regras que usaríamos agora para qualquer medição remota de longa duração:
- Dispare e esqueça, com resultados fora de banda. Os executores são invocados de forma assíncrona (a conexão fecha em milissegundos) e gravam seus resultados como itens em uma pequena tabela DynamoDB; o driver consulta em busca de oito linhas de resultado. Nenhuma conexão vive mais do que uma requisição.
- Retries desligados em todo lugar. O cliente de medição roda com uma tentativa por requisição — um retry absorveria em silêncio exatamente os throttles que existimos para contar — e o caminho de invocação também está com os retries desligados, então nenhum executor pode jamais executar duas vezes.
- Um watchdog em vez de um travamento. Cada executor corre seu cronograma contra um prazo; se algo emperra, ele retorna contagens parciais mais um retrato de exatamente onde ficou preso, em vez de estourar o tempo em silêncio. Uma execução que falha e se explica custa uma leitura; uma travada custa uma noite.
Cada requisição também carrega um timeout de 8 segundos. A execução que travou fez isso porque uma única requisição em voo sem timeout emperrou o passo final de drenagem para sempre. Uma espera sem limite, a cada ~4 milhões de requisições, foi o bastante.
O que as leituras fizeram
A fase de leitura rodou contra uma segunda tabela nova populada com 1,000 itens, usando GetItems (~1 KB cada, 0.5 unidade de leitura):
| Oferecido | Alcançado | Com 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 throttles, sempre. A linha de base documentada de 12,000 leituras/s se sustenta e não conseguimos achar o limite dela: a 16,000/s oferecidos, cinco dos nossos oito executores atingiram a própria saturação do lado do cliente, então o número de 12,762/s é onde a nossa frota estancou, não onde o DynamoDB estancou. Estamos dizendo isso com todas as letras em vez de vestir a informação como um limite do serviço. As leituras dentro da região rodaram a 2–4 ms de p50.
Dois números menores que vale guardar: uma tabela sob demanda nova foi de CreateTable a ACTIVE em 22 segundos na execução do benchmark e em 7.4 segundos em uma sondagem anterior — orce a variação, não o melhor caso. E o benchmark inteiro, 672,116 gravações faturadas e 1.08 milhão de leituras, custou $0.97. O instrumento é reutilizável; o experimento é um café.
Meia hora de pressão não dobra o teto
A regra de crescimento da AWS diz que a capacidade sob demanda acomoda até o dobro do seu pico anterior. Queríamos ver isso acontecer, então, depois da execução do teto, mantivemos uma tabela sob 8,000 gravações/s de carga oferecida por 34 minutos contíguos e agrupamos a taxa alcançada a cada 10 segundos. O formato:
| Minutos sob carga | Teto |
|---|---|
| 0–8 | ~4,000/s (linha de base, imóvel) |
| 9–25 | ~5,000/s |
| 26 | ~6,000/s |
| 27–34 | ~7,000/s |
O crescimento chega em degraus abruptos de ~1,000/s, não em rampa — um minuto é plano em uma taxa, o minuto seguinte é plano na taxa seguinte. O primeiro teto é teimoso por 8 minutos inteiros de demanda excedente contínua. E depois de 34 minutos a tabela entregou 7,000/s: 1.75× o ponto de partida, ainda abaixo dos 8,000 oferecidos e de uma duplicação limpa. Se o seu lançamento precisa de mais de ~4,000 gravações/s em uma tabela nova, aqueça-a com antecedência ou defina explicitamente a taxa máxima de transferência sob demanda dela — o mecanismo de crescimento é real, mas não é instantâneo nem generoso no seu cronograma. O comportamento também é estável: sob 9,000/s de carga ofertada, a tabela do estudo de 2019 tinha crescido até cerca de 7,000/s ao fim do teste — o mesmo platô que a nossa alcançou sete anos depois. Essa execução custou $12.67, a coisa mais cara que fizemos no dia todo.
O que vale para qualquer medição própria de um serviço de nuvem
- Coloque o gerador de carga na mesma região do alvo. Caso contrário, você está medindo a sua rota, não o serviço.
- Um processo Node estanca por volta de 2,000 requisições assinadas por segundo, independentemente da memória; distribua a carga entre executores e some resultados contados.
- Desligue os retries no caminho de medição, em todas as camadas. Retries existem para esconder exatamente aquilo que um benchmark existe para ver.
- Nunca mantenha uma conexão silenciosa por toda uma execução longa. Invoque de forma assíncrona, entregue os resultados fora de banda, consulte.
- Dê um timeout a cada requisição e um watchdog a cada executor, que retorne dados parciais com um retrato do estado travado.
- Defina um teto rígido de operações por executor, para que um bug de ritmo aborte em vez de inflar a conta, e destrua tudo o que a execução criou — tabelas, roles, funções, logs — em um
finally.
As páginas de referência que isso alimenta
O conjunto completo de dados — cada janela, cada executor, os percentis de latência, as strings de erro literais — agora sustenta as tabelas medidas na nossa referência de limites do DynamoDB, ao lado das sondagens de tamanho de item e de limite de página que publicamos antes. Se você trabalha com DynamoDB todo dia, o DynoTable é o nosso cliente de desktop para isso — o mesmo time, o mesmo hábito de conferir afirmações contra o serviço ao vivo antes de repeti-las.