Por qué un GSI de DynamoDB es eventualmente consistente
Escribes un elemento, consultas inmediatamente un índice secundario global para buscarlo,
y no obtienes nada de vuelta — aunque la escritura tuvo éxito y un GetItem sobre la
tabla base devuelve el elemento sin problemas.
Nada está roto. Has topado con la propiedad más sorprendente de los GSI: toda lectura de un GSI es eventualmente consistente. Hay una breve ventana tras una escritura en la que el índice todavía no se ha puesto al día.
¿Los GSI de DynamoDB son eventualmente consistentes?
Sí — toda lectura de un índice secundario global es eventualmente consistente, sin forma de desactivarlo. Tu escritura se confirma primero en la tabla base y luego se propaga de forma asíncrona al índice, así que una consulta lanzada justo después de una escritura puede devolver filas obsoletas o ausentes. DynamoDB no ofrece ningún flag ConsistentRead para un GSI.
- Un GSI es una tabla separada y replicada de forma asíncrona — tu escritura se confirma primero en la tabla base, luego se propaga al índice.
- No existe ningún flag
ConsistentReadpara un GSI. A diferencia de la tabla base, no puedes forzar una lectura fuerte para cerrar la brecha. - Lee tus propias escrituras desde la tabla base, no desde el GSI. Ya tienes la clave primaria justo después de una escritura.
- Impón la unicidad con una escritura condicional, no con una consulta a un GSI. La brecha de propagación convierte una comprobación de "¿está cogido?" en una carrera.
El síntoma: un registro que "no puede encontrarse a sí mismo"
Toma una tabla Members para un servicio de cuentas de usuario. La tabla base está
indexada por un id interno, pero los usuarios inician sesión por correo electrónico, así
que hay un GSI de búsqueda por correo:
| PK | SK | displayName | |
|---|---|---|---|
| ACC#a1f9c | PROFILE | ada@northwind.test | Ada L. |
| GSI1PK | GSI1SK |
|---|---|
| ada@northwind.test | ACC#a1f9c |
El flujo de registro hace dos cosas seguidas: PutItem del nuevo miembro, luego
Query EmailIndex WHERE GSI1PK = "ada@northwind.test" para comprobar que nadie más
reclamó esa dirección y para cargar el perfil.
Ejecuta esas dos llamadas con unos pocos milisegundos de diferencia y la Query puede
devolver cero elementos. Hazlo de nuevo un segundo después y la fila está ahí. La
escritura no falló — el índice simplemente todavía no se había actualizado.
Por qué ocurre esto: los GSI se replican de forma asíncrona
Un GSI es una tabla separada y gestionada internamente con sus propias particiones y su propio esquema de claves. No se mantiene dentro de la misma transacción que tu escritura en la tabla base.
Cuando haces PutItem, DynamoDB confirma de forma durable en la tabla base, reconoce tu
escritura, y luego propaga el cambio de forma asíncrona a cada GSI. La
documentación de GSI
de AWS lo afirma sin rodeos: los GSI solo soportan lecturas eventualmente consistentes.
El retraso de propagación entre una escritura en la tabla base y la actualización del índice suele ser una fracción de segundo — pero no está garantizado ni acotado bajo carga. Diseñar como si lo estuviera es la trampa.
Esto no es un bug; es el compromiso original del diseño de Dynamo. El artículo Amazon Dynamo de 2007 eligió disponibilidad y tolerancia a particiones por encima de la consistencia fuerte.
Los GSI heredan ese linaje. El acoplamiento débil es lo que permite que el índice escale y siga siendo escribible de forma independiente de la tabla base.
La brecha entre el 200 OK y "replicar cambio" es la ventana en la que tu lectura del
índice está obsoleta. No hay ningún flag de lectura consistente que la cierre.
A diferencia de la tabla base — donde pasas ConsistentRead = true para forzar un
GetItem/Query fuertemente consistente — un GSI rechaza rotundamente esa opción.
Un LSI sí puede leerse de forma fuerte porque comparte las particiones de la tabla base; consulta GSI vs LSI para entender por qué existe esa distinción.
Una trampa más sutil: valores antiguos obsoletos, no solo nuevos faltantes
El caso de la fila faltante es el obvio. El bug más silencioso es leer un valor anterior obsoleto.
Digamos que Ada cambia su correo de ada@northwind.test a ada.l@northwind.test. La
tabla base se actualiza atómicamente, pero por un momento el GSI todavía puede devolver la
entrada de índice antigua.
Una búsqueda contra el valor nuevo no encuentra nada, mientras que el valor abandonado todavía resuelve.
Peor: si consultas el GSI y reescribes basándote en lo que leíste, puedes actuar sobre un valor que ya no existe. Trata cualquier lectura de un GSI como una instantánea que puede ir con retraso respecto a la realidad.
Diséñalo para sortearlo — no luches contra ello
La ventana de propagación es real, así que el arreglo es arquitectónico, no una perilla de reintentos que activas. Cuatro patrones, aproximadamente en orden de preferencia:
Lee tus propias escrituras desde la tabla base. Justo después de una escritura ya tienes la clave primaria (
ACC#a1f9c), así que haz unGetItemfuertemente consistente sobre la tabla base en lugar de consultar el GSI.El GSI es para el otro patrón de acceso — "tengo un correo, encuentra la cuenta" — no para confirmar la escritura que acabas de hacer.
Impón la unicidad con un elemento guarda, no con el GSI. Nunca confíes en una consulta a un GSI para probar que un correo no está reclamado — la brecha de propagación convierte eso en una carrera que dos registros simultáneos pueden perder ambos.
En su lugar, escribe un elemento de unicidad dedicado indexado por el propio correo (
PK = "EMAIL#ada@northwind.test") dentro de unTransactWriteItemscon unaConditionExpressiondeattribute_not_exists(PK).Las condiciones fuertemente consistentes de la tabla base, aplicadas atómicamente, son lo que realmente impone la unicidad.
TransactWriteItems: - Put member item (PK = ACC#a1f9c, SK = PROFILE) - Put uniqueness item (PK = EMAIL#ada@northwind.test) ConditionExpression: attribute_not_exists(PK)Si un segundo registro compite por la misma dirección, su condición falla y toda la transacción se rechaza — sin GSI, sin retraso de propagación, sin doble reclamación.
Construye y previsualiza esa condición
attribute_not_existscon el Constructor de expresiones de DynamoDB antes de conectarla al código.Tolera el retraso en la UX. Cuando la lectura del GSI es genuinamente la herramienta correcta (inicio de sesión por correo de un usuario existente), la ventana es de menos de un segundo e inofensiva — una cuenta establecida se propagó hace mucho.
Reserva la ruta fuertemente consistente de la tabla base solo para el momento de lectura-tras-escritura.
Vuelve a consultar, no asumas. Si un flujo de trabajo debe observar un elemento recién-creado a través del GSI, trata un resultado vacío como "todavía no visible", no como "no existe", y vuelve a consultar tras un breve retroceso.
Pero prefiere los patrones 1 y 2, que eliminan las suposiciones por completo.
Ve tú mismo la brecha de propagación
La forma más rápida de construir intuición es verlo suceder. En DynoTable colocas un elemento en la tabla base e inmediatamente consultas el GSI en una segunda pestaña.
En una tabla con carga ocasionalmente pillarás el índice yendo por detrás de los datos base, y luego lo verás converger en la siguiente actualización.
Ver el retraso con tus propios datos hace que la regla "lee tus propias escrituras desde la tabla base" se quede mucho mejor que cualquier diagrama.
Trampas y próximos pasos
- No condiciones la lógica a una lectura-tras-escritura de un GSI. Las comprobaciones de unicidad, las confirmaciones de "¿aterrizó mi escritura?" y los bucles de lectura-modificación-escritura pertenecen a la tabla base fuertemente consistente.
- No recurras a
ConsistentReaden un GSI — no está permitido y dará error. - No modeles un patrón de acceso como un GSI cuando la clave base ya lo responde. Sirve una lectura desde la clave primaria y te saltas la ventana de propagación por completo.
Elegir la forma de clave correcta es todo el juego en el
diseño de tabla única; saber cuándo una Query gana a un
Scan te mantiene fuera del índice en primer lugar
(Query vs Scan).
Construye y prueba tu ConditionExpression de unicidad en el
Constructor de expresiones de DynamoDB. Luego
prueba DynoTable para ver las escrituras de la tabla base propagarse a un GSI
en tiempo real, y diseña tus claves para que la ventana de consistencia eventual nunca te
muerda.