El problema
Tu tabla está en modo bajo demanda, la gráfica de consumo total va holgada, y
aun así DynamoDB empieza a devolver ProvisionedThroughputExceededException.
No es un error de configuración ni una cuota de cuenta. Es que el techo que estás tocando no es el de la tabla: es el de una partición.
Cada partición de DynamoDB está diseñada para dar como máximo 3 000 unidades de lectura y 1 000 de escritura por segundo. Y como la partición se decide pasando la partition key por una función hash, todos los elementos con la misma clave viven en el mismo sitio físico y comparten ese techo.
Así que basta con una clave popular para agotarlo mientras el resto de la tabla está ociosa. Los sospechosos habituales son siempre los mismos:
- Una fecha:
PEDIDOS#2026-09-06recibe el 100 % de las escrituras del día. - Un tenant grande:
TENANT#acmees el 80 % de tu tráfico. - Un contador global:
CONTADOR#visitas, una sola clave para todo. - Un estado:
ESTADO#pendiente, donde caen todos los pedidos nuevos.
La solución
Ampliar el espacio de claves añadiendo un sufijo. AWS describe dos formas, y la diferencia entre ellas decide si podrás volver a leer un elemento concreto.
Sufijo aleatorio
Al escribir, eliges un número al azar entre 1 y N y lo concatenas: la clave
2026-09-06 se convierte en 2026-09-06.1, 2026-09-06.2, y así hasta
2026-09-06.200. Las escrituras se reparten solas y el throughput se multiplica
por N.
El precio: no sabes en qué sufijo quedó un elemento concreto. Para leerlo, tendrías que consultar los N.
Sufijo calculado
En vez de un número al azar, se calcula a partir de algo por lo que ya vas a
consultar. Si tus elementos tienen un OrderId, el sufijo sale de una función
determinista sobre él — AWS propone algo tan simple como el producto de los
puntos de código UTF-8 del identificador, módulo 200, más 1.
El reparto es igual de bueno, y además puedes reconstruir la clave: dado un
OrderId, recalculas el sufijo y haces un GetItem directo.
Lo que no cambia entre las dos estrategias: para leer todos los elementos de un día sigues teniendo que consultar los N sufijos y unir los resultados en tu código.
Cuándo usarlo
- Ya has medido que el problema es una clave concreta, no el volumen total.
- El acceso está concentrado por diseño: fecha, tenant, estado, contador.
- Las escrituras dominan. Para lecturas calientes, un caché —DAX o ElastiCache— suele salir mejor que reestructurar las claves.
Cuándo NO usarlo
Cómo implementarlo
- Identifica la clave caliente con Contributor Insights y con la métrica de peticiones estranguladas.
- Elige N con margen: escrituras por segundo previstas dividido entre 1 000, por un factor de holgura. Con 4 000/s, diez sufijos dejan 400 por clave.
- Decide la estrategia: calculado si necesitas leer elementos sueltos; aleatorio si solo escribes y lees por lotes.
- Encapsula el cálculo del sufijo en una función y no lo repitas por ahí: el día que cambie N, tiene que cambiar en un solo sitio.
- Escribe también la lectura por lotes —consultar los N y unir— porque alguien la va a necesitar.
- Documenta N junto al modelo. Sin ese número, la tabla es ilegible.
El código
const SUFIJOS = 10;
// Calculado, no aleatorio: dado el pedido podemos volver a encontrarlo.
// La formula es la que sugiere AWS — suma de puntos de codigo, modulo N.
const sufijo = (id: string) =>
([...id].reduce((a, c) => a + c.codePointAt(0)!, 0) % SUFIJOS) + 1;
const claveDe = (fecha: string, pedidoId: string) =>
`PEDIDOS#${fecha}.${sufijo(pedidoId)}`;
// ── Escribir: reparte solo ─────────────────────────────────────────
await ddb.send(new PutCommand({
TableName: 'app',
Item: { PK: claveDe(fecha, pedido.id), SK: `PEDIDO#${pedido.id}`, ...pedido },
}));
// ── Leer uno: la clave se reconstruye ──────────────────────────────
const uno = await ddb.send(new GetCommand({
TableName: 'app',
Key: { PK: claveDe(fecha, pedidoId), SK: `PEDIDO#${pedidoId}` },
}));
// ── Leer el dia entero: N consultas y una union ────────────────────
// Este es el precio del patron, y conviene tenerlo escrito una sola vez.
const dia = async (fecha: string) => {
const partes = await Promise.all(
Array.from({ length: SUFIJOS }, (_, i) =>
ddb.send(new QueryCommand({
TableName: 'app',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': `PEDIDOS#${fecha}.${i + 1}` },
})),
),
);
return partes.flatMap((p) => p.Items ?? []);
};
Te va a morder
Coste
| Concepto | Efecto |
|---|---|
| Escrituras | igual — el mismo número de elementos, repartidos |
| Lectura de un elemento | igual, con sufijo calculado |
| Lectura del conjunto | × N consultas |
| Almacenamiento | igual |
| Complejidad del modelo | sube, y es permanente |
El sharding no cuesta más en escritura: escribes lo mismo, solo que en sitios distintos. Lo que sube es el coste de las lecturas agregadas y, sobre todo, el coste humano — el modelo deja de ser evidente y hay que documentar N.
Y una comparación que conviene hacer antes: repartir la clave es gratis;
reordenar el modelo para que la consulta caliente no exista puede ser mejor.
Si ESTADO#pendiente está caliente porque consultas los pendientes cada
segundo, quizá el arreglo no es trocear esa clave sino mantener la lista de
pendientes en otro sitio.
Fuentes
Techo de 3 000 unidades de lectura y 1 000 de escritura por partición, y el ejemplo del elemento de 20 KB: Best practices for designing and using partition keys. Estrategias de sufijo aleatorio y sufijo calculado, la fórmula sugerida y el coste de leer el conjunto completo: Using write sharding to distribute workloads evenly. Capacidad por tabla y por cuenta: Quotas in Amazon DynamoDB.