🛡️Satisfait ou remboursé — Setup remboursé si pas satisfait après 30 jours

Deepthix
← Retour au blog
tech26 janvier 2026

PostgreSQL comme Dead Letter Queue : le DLQ qui se requête en SQL

Les DLQ Kafka sont souvent des boîtes noires. Stocker les événements en échec dans PostgreSQL te donne visibilité, audit et replay ciblé — avec un design simple et robuste.

Pourquoi ton DLQ Kafka finit en cimetière (et pourquoi c’est ton problème)

Dans un système event-driven, les échecs ne sont pas une exception. Ils sont statistiquement garantis. API de “hydration” en panne, timeouts, consumer qui crash au mauvais moment, payload incomplet, schéma qui drift… tout ça arrive, même quand tu as “bien fait les choses”.

Le réflexe classique : un topic Kafka “DLQ”. Sauf qu’en pratique, beaucoup d’équipes transforment ce DLQ en fosse commune. Les messages s’y empilent, personne ne sait répondre à des questions basiques (“qu’est-ce qui a cassé hier ?”, “combien d’échecs par type d’erreur ?”), et rejouer proprement un sous-ensemble devient un mini-projet.

C’est exactement le point soulevé dans le retour d’expérience publié fin 2025 sur un pipeline de reporting chez Wayfair : Kafka est excellent pour transporter des événements, mais une fois en DLQ, l’inspection et le tri deviennent pénibles sans tooling additionnel. Leur choix : utiliser PostgreSQL comme DLQ (CloudSQL sur GCP), en stockant les événements en échec dans une table dédiée, avec contexte et statut, pour rendre l’échec observable et actionnable (source : diljitpr.net).

DLQ : définition utile (pas la version PowerPoint)

Un Dead Letter Queue sert à isoler les messages qui n’arrivent pas à être traités correctement après une ou plusieurs tentatives. L’objectif n’est pas “d’éviter les erreurs”. L’objectif c’est :

  1. Ne pas bloquer le flux (ton consumer continue à traiter le reste).
  2. Ne pas perdre l’info (payload + contexte).
  3. Pouvoir diagnostiquer (requêtes, dashboards, audit).
  4. Pouvoir rejouer (ciblé, contrôlé, mesurable).

Selon une étude 2025 sur la maturité EDA, 94 % des implémentations matures utilisent une DLQ, et 82 % rejouent les événements après correction de la cause racine. En régime normal, la DLQ représente 0,01 % à 0,05 % du volume, mais peut monter à 0,1 % à 0,3 % pendant une perturbation (panne, déploiement foireux, dépendance lente). (source : ResearchGate, Event-Driven Architecture: The Backbone of Real-Time Enterprise Integration, 2025)

Traduction business : si tu n’as pas une DLQ exploitable, tu vas perdre du temps, de l’argent, et de la confiance.

Pourquoi PostgreSQL est un bon DLQ (quand tu veux du concret)

L’idée est simple : au lieu d’envoyer les messages en échec dans un topic DLQ, tu les persistes dans Postgres.

1) Visibilité immédiate

Avec Postgres, ton DLQ est SQL-native :

  • “Top 10 des erreurs depuis 24h”
  • “Tous les events d’un customer_id qui ont échoué”
  • “Échecs uniquement sur le type OrderCreated
  • “Échecs après le déploiement X”

Tu passes d’un flux opaque à une base interrogable.

2) Durabilité + audit

Postgres te donne la durabilité ACID, des contraintes, du versioning de schéma, et un audit facile. Pour des systèmes qui alimentent des rapports financiers / opérationnels, c’est un énorme plus.

3) Moins d’infra, moins de bullshit

Si tu as déjà Postgres (c’est le cas de 90 % des SaaS), tu ajoutes une table et deux index, pas un nouveau cluster, pas un nouveau provider, pas une nouvelle surface d’attaque.

4) Reprocessing contrôlé avec FOR UPDATE SKIP LOCKED

Le pattern que Wayfair met en avant (et qui marche très bien) : plusieurs workers peuvent récupérer des messages en échec sans se marcher dessus via :

SQL
SELECT id, payload
FROM dlq_events
WHERE status = 'PENDING'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 100;

SKIP LOCKED évite que deux workers prennent la même ligne, sans orchestration externe.

Le design d’une table DLQ qui ne te sabotera pas

Voici un schéma pragmatique (à adapter) :

SQL
CREATE TYPE dlq_status AS ENUM ('PENDING', 'PROCESSING', 'SUCCEEDED', 'FAILED', 'GAVE_UP');

CREATE TABLE dlq_events (
  id            BIGSERIAL PRIMARY KEY,
  event_key     TEXT,                 -- idempotency key (si tu en as)
  event_type    TEXT NOT NULL,
  source        TEXT,                 -- consumer/service
  payload       JSONB NOT NULL,
  error_code    TEXT,
  error_message TEXT,
  error_stack   TEXT,
  status        dlq_status NOT NULL DEFAULT 'PENDING',
  attempts      INT NOT NULL DEFAULT 0,
  next_retry_at TIMESTAMPTZ,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX dlq_events_status_retry_idx
  ON dlq_events (status, next_retry_at, created_at);

CREATE INDEX dlq_events_type_idx
  ON dlq_events (event_type, created_at);

CREATE INDEX dlq_events_payload_gin
  ON dlq_events USING GIN (payload);

Champs qui font vraiment la différence

  • event_key : si tu veux rejouer sans créer de doublons, c’est ton filet de sécurité (idempotence côté “write”).
  • attempts + next_retry_at : tu implémentes un backoff et tu évites le retry en boucle.
  • payload JSONB + index GIN : tu peux filtrer sur un champ métier sans ETL.

Le workflow qui marche en prod

1) Échec détecté → insert en DLQ

Tu captures : payload brut + contexte + cause.

  • API downstream down → error_code = DOWNSTREAM_TIMEOUT
  • schéma invalide → error_code = VALIDATION_ERROR

2) Reprocessor (cron / worker) → lock + processing

Pseudo-flow :

  1. SELECT ... FOR UPDATE SKIP LOCKED sur les PENDING dont next_retry_at <= now()
  2. UPDATE status='PROCESSING', attempts=attempts+1
  3. tente le traitement
  4. succès → SUCCEEDED
  5. échec → PENDING avec next_retry_at = now() + backoff
  6. au-delà de N tentatives → GAVE_UP (et alerte)

3) Observabilité : tu alertes sur le temps passé en DLQ

Le KPI utile n’est pas “nombre de messages en DLQ”. C’est :

  • âge max des PENDING
  • taux d’entrée DLQ (par type d’événement)
  • taux de succès au reprocess

Si un message reste PENDING 2h alors que ton SLA est 10 min : alerte.

Quand PostgreSQL DLQ est un excellent choix (et quand c’est une mauvaise idée)

Excellent si :

  • tu as un débit modéré à élevé, mais pas du “centaines de milliers d’events/sec”
  • tu veux diagnostic + replay ciblé sans tooling Kafka custom
  • tu as déjà Postgres en datastore durable (cas Wayfair)
  • ton équipe veut réduire la complexité infra

Mauvaise idée si :

  • tu as un DLQ qui peut contenir des millions de lignes longtemps sans purge : tu vas souffrir (bloat, index, VACUUM)
  • tu veux du multi-région “global active-active” pour la DLQ : Postgres n’est pas Kafka
  • tu as besoin d’un système de queue complet (consumer groups massifs, retention streaming, etc.)

La communauté le résume bien : FOR UPDATE SKIP LOCKED est solide pour small-to-medium workloads, mais si la table grossit trop, il faut partitionner et nettoyer régulièrement (retours Reddit cités dans la recherche web).

Les patterns anti-emmerdes : partitionnement, rétention, et “bloat hygiene”

1) Partitionner par date (ou par statut)

Si tu gardes les DLQ 30 jours, partitionne par semaine/mois :

  • requêtes plus rapides
  • purge simple (DROP PARTITION)

2) Rétention agressive + archivage

Garde en base ce qui est actionnable. Le reste : export S3/GCS (par jour) et basta.

3) Indexer pour tes requêtes réelles

Ne mets pas 12 index “au cas où”. Mesure tes requêtes :

  • status + next_retry_at + created_at pour le reprocess
  • event_type + created_at pour l’analyse
  • GIN JSONB seulement si tu l’utilises

Et si tu veux aller plus loin : PGMQ, Queen, et l’approche hybride

Il y a une tendance claire fin 2025–début 2026 : hybrider.

  • Kafka/SQS/RabbitMQ pour le transport à haut débit
  • Postgres pour la durabilité, l’audit, et la gestion “opérationnelle” des échecs

Des projets comme Queen (queue open-source sur Postgres) poussent l’idée plus loin : consumer groups, replay, exactly-once, DLQ intégrée. Et côté écosystème Postgres, PGMQ (popularisée via Supabase) montre que beaucoup d’équipes préfèrent “un outil en moins” quand le besoin est pragmatique.

Le bon réflexe : ne pas être dogmatique. Tu gardes Kafka pour ce qu’il fait le mieux, et tu utilises Postgres pour ce qu’il fait le mieux.

Checklist actionnable (tu peux l’implémenter cette semaine)

  1. Crée une table DLQ avec status, attempts, next_retry_at, payload JSONB.
  2. Écris un reprocessor (worker) avec FOR UPDATE SKIP LOCKED.
  3. Ajoute l’idempotence côté write (event_key / unique constraint si possible).
  4. Mets des alertes sur l’âge max des PENDING + taux d’entrée.
  5. Planifie la rétention (partition + purge) dès le jour 1.

Si tu fais ça, ton DLQ arrête d’être un cimetière. Il devient un outil de contrôle qualité sur ton système distribué.

Tu veux automatiser tes opérations avec l'IA ? Réserve un call de 15 min pour en discuter.

PostgreSQL DLQDead Letter Queueevent-driven architectureKafka consumer retriesFOR UPDATE SKIP LOCKED

Tu veux automatiser tes opérations ?

Discutons de ton projet en 15 minutes.

Réserver un call