Erreur 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.
Cette erreur n'existe qu'aux niveaux d'isolation Repeatable Read et Serializable, et c'est ces niveaux qui fonctionnent comme prévu : PostgreSQL n'a pas pu produire un résultat cohérent avec les garanties que vous avez demandées, il a donc annulé votre transaction plutôt que de renvoyer silencieusement quelque chose de faux.
Ce que signifie cette erreur
Les deux variantes correspondent aux deux niveaux :
- « due to concurrent update » (Repeatable Read) : votre transaction a tenté de modifier une ligne qu'une autre transaction a changée et validée après la prise de votre snapshot. PostgreSQL ne peut pas appliquer votre écriture à des données que vous n'avez jamais vues, il vous annule donc.
- « due to read/write dependencies among transactions » (Serializable) : la machinerie SSI a trouvé un motif de lectures et d'écritures parmi des transactions concurrentes qui ne pouvait correspondre à aucun ordre d'exécution un-à-la-fois. Notez la formulation présente dans la documentation et en pratique : Serializable peut annuler des transactions préventivement — des faux positifs occasionnels font partie du marché.
Dans les deux cas, la transaction a subi un rollback et, comme le dit le HINT, réussirait probablement si on la relançait simplement. Le SQLSTATE est 40001 dans les deux cas.
Causes fréquentes
- Fonctionner en Repeatable Read ou Serializable avec des processus d'écriture concurrents sur les mêmes lignes — les lignes chaudes et les compteurs sont les points de collision habituels.
- Transactions longues : plus votre snapshot est ancien, plus il est probable que quelqu'un ait validé un changement en conflit entre-temps.
- En Serializable, les requêtes qui lisent largement : un parcours séquentiel pose un verrou de prédicat sur toute la table, il entre donc en conflit avec toute écriture concurrente sur cette table, pertinente ou non.
- Un ORM ou un framework fixant silencieusement le niveau d'isolation — les équipes découvrent parfois qu'elles fonctionnent en Repeatable Read seulement quand cette erreur apparaît sous charge.
Comment la diagnostiquer
- Confirmez à quel niveau vous fonctionnez réellement :
SHOW default_transaction_isolation;et vérifiez si l'application le fixe par transaction. - Journalisez et comptez les occurrences du SQLSTATE
40001: un faible taux de fond sous Serializable est normal et sain ; un pic lié à un endpoint identifie la charge de travail en collision. - Identifiez les deux côtés du conflit à partir des journaux applicatifs (quelles transactions touchent les mêmes lignes ou tables autour du moment de l'échec). Les codes de raison dans le
DETAILde l'erreur sont surtout utiles pour confirmer que c'est bien SSI à l'œuvre, pas pour pointer vers le pair.
Comment la corriger
- Réessayez toute la transaction sur le SQLSTATE
40001— y compris ses lectures, puisque les valeurs lues la première fois sont précisément ce qui a pu changer. Tentatives bornées, petit backoff, pas d'effets de bord (e-mails, appels HTTP) dans le bloc réessayé. - Gardez les transactions courtes et touchez le moins de lignes possible.
- En Serializable, faites en sorte que les lectures utilisent des index : des parcours d'index précis posent des verrous de prédicat étroits et entrent bien moins en collision que les parcours séquentiels.
- Si un point chaud génère des réessais constants, envisagez de traiter ce chemin particulier avec Read Committed plus un verrouillage explicite (
SELECT ... FOR UPDATE) à la place — la contention se résout en attendant plutôt qu'en annulant.
🔍
À lire aussi : Les niveaux d'isolation des transactions en pratique — ce que garantit chaque niveau, le write skew, et une section complète sur réessayer correctement.
