Error de PostgreSQL: could not serialize access
ERROR: could not serialize access due to concurrent update
-- oppure, a livello Serializable:
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
Este error solo existe en los niveles de aislamiento Repeatable Read y Serializable, y es esos niveles funcionando según su diseño: PostgreSQL no pudo producir un resultado consistente con las garantías que pediste, así que abortó tu transacción en lugar de devolver silenciosamente algo incorrecto.
Qué significa este error
Las dos variantes corresponden a los dos niveles:
- "due to concurrent update" (Repeatable Read): tu transacción intentó modificar una fila que otra transacción cambió e hizo commit después de que se tomara tu snapshot. PostgreSQL no puede aplicar tu escritura a datos que nunca viste, así que te aborta.
- "due to read/write dependencies among transactions" (Serializable): la maquinaria SSI encontró un patrón de lecturas y escrituras entre transacciones concurrentes que no podía corresponder a ningún orden de ejecución de una en una. Fíjate en la frase que aparece en la documentación y en la práctica: Serializable puede abortar transacciones preventivamente — algún falso positivo ocasional es parte del trato.
En cualquier caso la transacción se revirtió y, como dice el HINT, probablemente tendría éxito si simplemente se ejecuta de nuevo. El SQLSTATE es 40001 en ambos casos.
Causas comunes
- Ejecutar en Repeatable Read o Serializable con escritores concurrentes sobre las mismas filas — las filas calientes y los contadores son los puntos de colisión habituales.
- Transacciones largas: cuanto más viejo sea tu snapshot, más probable es que alguien haya hecho commit de un cambio en conflicto mientras tanto.
- En Serializable, consultas que leen ampliamente: un escaneo secuencial toma un predicate lock sobre toda la tabla, así que entra en conflicto con cualquier escritura concurrente a esa tabla, relevante o no.
- Un ORM o framework configurando silenciosamente el nivel de aislamiento — los equipos a veces descubren que llevaban ejecutando Repeatable Read solo cuando este error aparece bajo carga.
Cómo diagnosticarlo
- Confirma en qué nivel estás ejecutando realmente:
SHOW default_transaction_isolation;y comprueba si la aplicación lo establece por transacción. - Registra y cuenta las ocurrencias del SQLSTATE
40001: una tasa baja de fondo en Serializable es normal y sana; un pico atado a un endpoint identifica la carga de trabajo que colisiona. - Identifica los dos lados del conflicto a partir de los logs de la aplicación (qué transacciones tocan las mismas filas o tablas alrededor del momento del fallo). Los reason codes del
DETAILdel error sirven sobre todo para confirmar que es SSI en acción, no para señalar al peer.
Cómo solucionarlo
- Reintenta la transacción completa ante el SQLSTATE
40001— incluidas sus lecturas, ya que los valores leídos la primera vez son exactamente lo que puede haber cambiado. Intentos acotados, backoff pequeño, sin efectos secundarios (emails, llamadas HTTP) dentro del bloque reintentado. - Mantén las transacciones cortas y toca la menor cantidad de filas posible.
- En Serializable, haz que las lecturas usen índices: los escaneos de índice precisos toman predicate locks estrechos y colisionan mucho menos que los escaneos secuenciales.
- Si un punto caliente genera reintentos constantes, considera manejar esa ruta concreta con Read Committed más bloqueo explícito (
SELECT ... FOR UPDATE) en su lugar — la contención se resuelve esperando en vez de abortando.
🔍
Lectura relacionada: Niveles de aislamiento de transacciones en la práctica — qué garantiza cada nivel, el write skew, y una sección completa sobre reintentar correctamente.
