Creazione di un sistema di distribuzione tramite webhook: postback affidabili nel 2026

Una guida pratica del 2026 per la consegna affidabile di webhook e affiliati postback con code, tentativi di ripetizione con jitter, circuit breaker, idempotenza, firme e telemetria della latenza per endpoint.
Sistema di consegna tramite webhook - Creazione di un sistema di consegna tramite webhook: postback affidabili nel 2026

Ultimo aggiornamento il 24 giugno 2026 a cura di Cesare Fikson

Risposta diretta: Un servizio di consegna affidabile per webhook o affiliati postback necessita di una coda robusta, tentativi limitati con jitter, interruzione del circuito per ogni endpoint, protezione dai duplicati, verifica della firma e telemetria che mostri se la consegna sta rallentando prima che fallisca.

Questa implementazione mantiene il modello di esecuzione volutamente snello: worker Python e PostgreSQL per la gestione della coda, della cronologia delle consegne e dello stato degli endpoint. Rappresenta un punto di partenza pratico per SaaS B2B, operazioni di affiliazione nel settore iGaming e qualsiasi prodotto che debba gestire eventi in uscita senza considerare un singolo timeout HTTP come una conversione persa.

Cosa deve garantire il contratto di fornitura

Controllate Perché è importante Verifica operativa
Registrazione durevole dell'evento Gli eventi persistono anche dopo il riavvio del worker. Ogni consegna accettata ha un ID e uno stato stabili.
Richiesta firmata I destinatari possono verificare il mittente e rilevare eventuali manomissioni del corpo del pacco. Utilizza una firma con data e ora e rifiuta le richieste obsolete.
Protezione duplicata I tentativi ripetuti possono altrimenti generare conversioni o aggiornamenti duplicati. Invia un ID evento e rendi il ricevitore idempotente.
Tentativi limitati I guasti temporanei vengono ripristinati senza sovraccaricare un endpoint degradato. In caso di incertezza, interrompere l'azione; fermarsi dopo aver raggiunto il limite di tentativi documentato.
Telemetria degli endpoint La sola profondità della coda può nascondere i partner lenti o in avaria. Monitora il tasso di successo, l'evento in sospeso più vecchio e i percentili di latenza per ciascun endpoint.

La sicurezza e i controlli duplicati hanno la precedenza sulla regolazione dei tentativi.

Trattate il corpo della richiesta in uscita come dati operativi sensibili. Firmate il corpo esatto della richiesta, includete un timestamp di consegna e un ID evento immutabile, ruotate i segreti di firma e assicuratevi che l'applicazione ricevente ignori in modo sicuro la riproduzione dello stesso evento. Una risposta HTTP positiva non è prova che un evento aziendale sia stato applicato esattamente una volta; questa decisione spetta al lato ricevente.

Per un flusso di lavoro di postback di affiliazione o iGaming, è consigliabile non includere nei log gli ID dei clic, gli ID di conversione e i campi rilevanti per i pagamenti, a meno che non siano specificati esplicitamente controlli di accesso e regole di conservazione. Il riferimento a Scaleo riportato di seguito è rilevante solo come esempio di latenza per il postback di affiliazione; non sostituisce la documentazione del contratto di fornitura tra i propri servizi.

Riferimenti utili per l'implementazione: Verifica della firma tramite webhook di Stripe, PostgreSQL SELECT e SKIP LOCKEDe Linee guida AWS su backoff esponenziale e jitter.

Il modello dati

Tutto inizia con due tabelle: una per la coda di consegna e una per le misurazioni della latenza.

sql

-- Pending and in-flight webhook deliveries
CREATE TABLE webhook_queue (
    id              SERIAL PRIMARY KEY,
    endpoint_id     INTEGER NOT NULL,
    endpoint_url    TEXT NOT NULL,
    payload         JSONB NOT NULL,
    
    -- Delivery state
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
        -- pending, in_flight, delivered, failed, dead_letter
    attempt_count   INTEGER NOT NULL DEFAULT 0,
    max_attempts    INTEGER NOT NULL DEFAULT 5,
    
    -- Timing
    created_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    next_attempt_at TIMESTAMP NOT NULL DEFAULT NOW(),
    delivered_at    TIMESTAMP,
    last_error      TEXT,
    
    -- Response tracking
    last_status_code INTEGER,
    last_response_ms INTEGER,  -- response time in milliseconds
    
    INDEX idx_queue_next (status, next_attempt_at)
        WHERE status IN ('pending', 'failed')
);

-- Per-endpoint latency measurements (ring buffer)
CREATE TABLE endpoint_latency (
    id              SERIAL PRIMARY KEY,
    endpoint_id     INTEGER NOT NULL,
    response_ms     INTEGER NOT NULL,
    status_code     INTEGER,
    measured_at     TIMESTAMP NOT NULL DEFAULT NOW(),
    
    INDEX idx_latency_endpoint (endpoint_id, measured_at)
);

-- Endpoint health state (circuit breaker)
CREATE TABLE endpoint_health (
    endpoint_id         INTEGER PRIMARY KEY,
    endpoint_url        TEXT NOT NULL,
    state               VARCHAR(20) NOT NULL DEFAULT 'closed',
        -- closed (healthy), open (broken), half_open (testing)
    consecutive_failures INTEGER NOT NULL DEFAULT 0,
    failure_threshold   INTEGER NOT NULL DEFAULT 10,
    last_failure_at     TIMESTAMP,
    last_success_at     TIMESTAMP,
    opened_at           TIMESTAMP,  -- when circuit opened
    cooldown_seconds    INTEGER NOT NULL DEFAULT 300,  -- 5 min before half_open
    
    -- Latency stats (updated periodically)
    p50_ms              INTEGER,
    p95_ms              INTEGER,
    p99_ms              INTEGER,
    sample_count        INTEGER NOT NULL DEFAULT 0
);

Tre cose da notare riguardo a questo schema.

In primo luogo, la webhook_queue la tabella utilizza un next_attempt_at colonna invece di un meccanismo di pianificazione separato. Il lavoratore esegue il polling delle righe in cui status IN ('pending', 'failed') AND next_attempt_at <= NOW()Questa è una coda di ritardo "fai da te" e funziona bene fino a circa 10,000 consegne al minuto. Oltre tale limite, è necessario utilizzare un message broker adeguato.

In secondo luogo, la endpoint_latency La tabella funge da buffer circolare. Periodicamente elimino le righe più vecchie di 24 ore. I percentili di latenza in endpoint_health I valori sono calcolati a partire da questa finestra temporale mobile e rappresentano l'andamento recente, non le medie storiche.

Terzo, l' endpoint_health La tabella implementa la macchina a stati dell'interruttore automatico. Maggiori dettagli di seguito.

Il fattorino

Il ciclo di lavoro principale è volutamente semplice. La complessità risiede nella logica di ritentativo e nell'interruttore automatico, non nel percorso di consegna stesso.

python

import requests
import time
import psycopg2
from psycopg2.extras import RealDictCursor
from datetime import datetime, timedelta

DB_DSN = "postgresql://user:pass@localhost/webhooks"

def get_connection():
    return psycopg2.connect(DB_DSN)

def deliver_webhooks(batch_size=50):
    """
    Fetch pending webhooks and attempt delivery.
    Uses SELECT FOR UPDATE SKIP LOCKED for safe concurrent workers.
    """
    conn = get_connection()
    cur = conn.cursor(cursor_factory=RealDictCursor)
    
    try:
        cur.execute("""
            SELECT id, endpoint_id, endpoint_url, payload, 
                   attempt_count, max_attempts
            FROM webhook_queue
            WHERE status IN ('pending', 'failed')
              AND next_attempt_at <= NOW()
            ORDER BY next_attempt_at ASC
            LIMIT %s
            FOR UPDATE SKIP LOCKED
        """, (batch_size,))
        
        rows = cur.fetchall()
        
        for row in rows:
            # Check circuit breaker before attempting
            if is_circuit_open(cur, row['endpoint_id']):
                # Don't attempt delivery — reschedule for after cooldown
                reschedule_for_cooldown(cur, row['id'], row['endpoint_id'])
                continue
            
            # Attempt delivery and measure latency
            result = attempt_delivery(
                row['endpoint_url'], 
                row['payload']
            )
            
            # Record latency measurement regardless of success/failure
            record_latency(
                cur, 
                row['endpoint_id'], 
                result['response_ms'], 
                result['status_code']
            )
            
            if result['success']:
                mark_delivered(cur, row['id'], result)
                record_success(cur, row['endpoint_id'])
            else:
                handle_failure(
                    cur, row['id'], row['endpoint_id'],
                    row['attempt_count'], row['max_attempts'],
                    result
                )
        
        conn.commit()
    
    except Exception as e:
        conn.rollback()
        raise
    finally:
        cur.close()
        conn.close()


def attempt_delivery(url, payload):
    """
    Fire the webhook and measure response time.
    Returns dict with success, status_code, response_ms, error.
    """
    start = time.monotonic()
    
    try:
        response = requests.post(
            url,
            json=payload,
            timeout=15,          # 15 second hard timeout
            headers={
                'Content-Type': 'application/json',
                'User-Agent': 'WebhookDelivery/1.0',
                'X-Delivery-Timestamp': str(int(time.time()))
            }
        )
        
        elapsed_ms = int((time.monotonic() - start) * 1000)
        
        return {
            'success': 200 <= response.status_code < 300,
            'status_code': response.status_code,
            'response_ms': elapsed_ms,
            'error': None if response.ok else f"HTTP {response.status_code}"
        }
    
    except requests.Timeout:
        elapsed_ms = int((time.monotonic() - start) * 1000)
        return {
            'success': False,
            'status_code': None,
            'response_ms': elapsed_ms,
            'error': 'timeout_15s'
        }
    
    except requests.ConnectionError as e:
        elapsed_ms = int((time.monotonic() - start) * 1000)
        return {
            'success': False,
            'status_code': None,
            'response_ms': elapsed_ms,
            'error': f'connection_error: {str(e)[:200]}'
        }

Migliori SELECT FOR UPDATE SKIP LOCKED La clausola è fondamentale per eseguire più istanze di lavoro. Senza SKIP LOCKEDIn questo modo, due worker si bloccherebbero sulla stessa riga. Ogni worker, invece, preleva un diverso batch di webhook in sospeso. Ciò consente la scalabilità orizzontale semplicemente avviando più processi worker.

Migliori time.monotonic() chiamare invece di time.time() è intenzionale. time.time() può verificarsi un salto indietro durante le regolazioni NTP. time.monotonic() non torna mai indietro, il che è importante quando si misurano latenze inferiori al secondo.

Logica di ripetizione con backoff esponenziale e jitter

Quando una consegna fallisce, i tempi di ripetizione determinano se il sistema si ripristina correttamente o se si crea un caos totale che sovraccarica il punto di accesso in difficoltà.

python

import random

def calculate_next_attempt(attempt_count, base_delay=30, max_delay=3600):
    """
    Exponential backoff with full jitter.
    
    Attempt 1: 0-30s
    Attempt 2: 0-60s  
    Attempt 3: 0-120s
    Attempt 4: 0-240s
    Attempt 5: 0-480s (capped at max_delay)
    
    Full jitter prevents thundering herd when an endpoint
    recovers and hundreds of retries fire simultaneously.
    """
    exponential_delay = base_delay * (2 ** attempt_count)
    capped_delay = min(exponential_delay, max_delay)
    jittered_delay = random.uniform(0, capped_delay)
    
    return datetime.utcnow() + timedelta(seconds=jittered_delay)


def handle_failure(cur, webhook_id, endpoint_id, 
                   attempt_count, max_attempts, result):
    """
    Handle a failed delivery attempt.
    Either retry with backoff or move to dead letter queue.
    """
    new_attempt_count = attempt_count + 1
    
    if new_attempt_count >= max_attempts:
        # Exhausted retries — dead letter
        cur.execute("""
            UPDATE webhook_queue 
            SET status = 'dead_letter',
                attempt_count = %s,
                last_error = %s,
                last_status_code = %s,
                last_response_ms = %s
            WHERE id = %s
        """, (
            new_attempt_count, result['error'],
            result['status_code'], result['response_ms'],
            webhook_id
        ))
    else:
        # Schedule retry with backoff
        next_attempt = calculate_next_attempt(new_attempt_count)
        cur.execute("""
            UPDATE webhook_queue
            SET status = 'failed',
                attempt_count = %s,
                next_attempt_at = %s,
                last_error = %s,
                last_status_code = %s,
                last_response_ms = %s
            WHERE id = %s
        """, (
            new_attempt_count, next_attempt,
            result['error'], result['status_code'],
            result['response_ms'], webhook_id
        ))
    
    # Update circuit breaker
    record_failure(cur, endpoint_id)

Perché utilizzare il jitter completo invece del jitter decorrelato o del jitter uguale? AWS ha pubblicato un'analisi definitiva in merito. Il jitter completo (randomizzazione tra 0 e il limite esponenziale) produce il tempo di completamento totale più basso su tutti i client. Il jitter uguale (randomizzazione tra metà del limite e il limite completo) è più conservativo ma più lento nello svuotare il backlog dei tentativi. Per la consegna di webhook in cui si hanno molti endpoint indipendenti, il jitter completo è la scelta giusta perché i tentativi di ciascun endpoint sono indipendenti: non è necessario coordinarli.

Interruttore automatico: smettete di insistere sui punti terminali danneggiati.

Il pattern circuit breaker impedisce al sistema di sprecare risorse su endpoint che falliscono ripetutamente. Senza di esso, un endpoint non funzionante accumula centinaia di tentativi in ​​sospeso che vanno in timeout dopo 15 secondi ciascuno, consumando la capacità dei worker in consegne che non andranno mai a buon fine.

python

def is_circuit_open(cur, endpoint_id):
    """
    Check if the circuit breaker is open (endpoint is broken).
    If open and cooldown has passed, transition to half_open.
    """
    cur.execute("""
        SELECT state, opened_at, cooldown_seconds
        FROM endpoint_health
        WHERE endpoint_id = %s
    """, (endpoint_id,))
    
    row = cur.fetchone()
    if not row:
        return False  # no health record = assume healthy
    
    if row['state'] == 'closed':
        return False
    
    if row['state'] == 'open':
        # Check if cooldown period has elapsed
        if row['opened_at'] and row['cooldown_seconds']:
            elapsed = (datetime.utcnow() - row['opened_at']).total_seconds()
            if elapsed >= row['cooldown_seconds']:
                # Transition to half_open — allow one probe
                cur.execute("""
                    UPDATE endpoint_health
                    SET state = 'half_open'
                    WHERE endpoint_id = %s
                """, (endpoint_id,))
                return False  # allow the probe delivery
        return True  # still in cooldown
    
    if row['state'] == 'half_open':
        return False  # allow probe delivery
    
    return False


def record_failure(cur, endpoint_id):
    """
    Record a delivery failure. Open circuit if threshold reached.
    """
    cur.execute("""
        UPDATE endpoint_health
        SET consecutive_failures = consecutive_failures + 1,
            last_failure_at = NOW()
        WHERE endpoint_id = %s
        RETURNING consecutive_failures, failure_threshold, state
    """, (endpoint_id,))
    
    row = cur.fetchone()
    if not row:
        # Create health record on first failure
        cur.execute("""
            INSERT INTO endpoint_health (endpoint_id, endpoint_url, 
                consecutive_failures, last_failure_at)
            VALUES (%s, '', 1, NOW())
            ON CONFLICT (endpoint_id) DO UPDATE
            SET consecutive_failures = endpoint_health.consecutive_failures + 1,
                last_failure_at = NOW()
        """, (endpoint_id,))
        return
    
    if row['state'] == 'half_open':
        # Probe failed — re-open circuit with longer cooldown
        cur.execute("""
            UPDATE endpoint_health
            SET state = 'open',
                opened_at = NOW(),
                cooldown_seconds = LEAST(cooldown_seconds * 2, 3600)
            WHERE endpoint_id = %s
        """, (endpoint_id,))
    
    elif (row['state'] == 'closed' and 
          row['consecutive_failures'] >= row['failure_threshold']):
        # Threshold reached — open circuit
        cur.execute("""
            UPDATE endpoint_health
            SET state = 'open',
                opened_at = NOW(),
                cooldown_seconds = 300  -- reset to 5 minutes
            WHERE endpoint_id = %s
        """, (endpoint_id,))


def record_success(cur, endpoint_id):
    """
    Record a delivery success. Close circuit if half_open.
    """
    cur.execute("""
        UPDATE endpoint_health
        SET consecutive_failures = 0,
            last_success_at = NOW(),
            state = 'closed'
        WHERE endpoint_id = %s
    """, (endpoint_id,))

La gestione dello stato half_open → open con cooldown raddoppiato è un dettaglio che la maggior parte delle implementazioni trascura. Se un endpoint fallisce durante il probe (stato half_open), non si vuole riprovare dopo altri 5 minuti. L'endpoint è ancora guasto. Raddoppiare il cooldown a 10 minuti, poi a 20, fino a un massimo di 1 ora. Questo impedisce al circuit breaker di trasformarsi in un meccanismo di martellamento periodico.

Tracciamento della latenza in percentili

Le medie mentono. Un endpoint con un tempo di risposta medio di 200 ms potrebbe rispondere in 50 ms nel 95% dei casi e in 3,000 ms nel restante 5%. La media sembra a posto. Il P95 rivela un problema che interessa 1 consegna su 20.

python

def record_latency(cur, endpoint_id, response_ms, status_code):
    """
    Record a latency measurement and update percentile stats.
    """
    cur.execute("""
        INSERT INTO endpoint_latency 
            (endpoint_id, response_ms, status_code)
        VALUES (%s, %s, %s)
    """, (endpoint_id, response_ms, status_code))


def update_latency_percentiles(cur, endpoint_id, window_hours=24):
    """
    Calculate P50, P95, P99 from the rolling window.
    Uses PostgreSQL's percentile_cont for exact percentiles.
    """
    cur.execute("""
        SELECT 
            COUNT(*) as sample_count,
            percentile_cont(0.50) WITHIN GROUP 
                (ORDER BY response_ms) AS p50,
            percentile_cont(0.95) WITHIN GROUP 
                (ORDER BY response_ms) AS p95,
            percentile_cont(0.99) WITHIN GROUP 
                (ORDER BY response_ms) AS p99
        FROM endpoint_latency
        WHERE endpoint_id = %s
          AND measured_at >= NOW() - INTERVAL '%s hours'
    """, (endpoint_id, window_hours))
    
    row = cur.fetchone()
    
    if row and row['sample_count'] > 0:
        cur.execute("""
            UPDATE endpoint_health
            SET p50_ms = %s,
                p95_ms = %s,
                p99_ms = %s,
                sample_count = %s
            WHERE endpoint_id = %s
        """, (
            int(row['p50']), int(row['p95']), 
            int(row['p99']), row['sample_count'],
            endpoint_id
        ))
    
    return row


def get_slow_endpoints(cur, p95_threshold_ms=2000):
    """
    Find endpoints whose P95 latency exceeds the threshold.
    These are candidates for investigation or circuit opening.
    """
    cur.execute("""
        SELECT endpoint_id, endpoint_url, 
               p50_ms, p95_ms, p99_ms, sample_count,
               state, consecutive_failures
        FROM endpoint_health
        WHERE p95_ms > %s
          AND sample_count >= 20  -- need sufficient samples
        ORDER BY p95_ms DESC
    """, (p95_threshold_ms,))
    
    return cur.fetchall()

PostgreSQL percentile_cont è una funzione di aggregazione di insiemi ordinati che calcola i percentili esatti. Per set di dati di grandi dimensioni, si passerebbe a percentile_disc (che restituisce un valore effettivamente osservato anziché interpolarlo) oppure utilizzare un'approssimazione t-digest. Per la consegna tramite webhook con una finestra di 24 ore, i percentili esatti sui dati grezzi sono sufficientemente veloci fino a circa 100,000 misurazioni per endpoint.

Migliori get_slow_endpoints Questa funzione è quella che eseguo come controllo programmato ogni 15 minuti. Gli endpoint con P95 superiore a 2 secondi vengono segnalati per un'indagine. Agli endpoint con P95 superiore a 5 secondi viene ridotta la soglia del circuit breaker: sono consentiti meno errori consecutivi prima che il circuito si apra, perché ogni consegna non riuscita impegna un thread di lavoro per l'intera durata del timeout.

Monitoraggio dello stato di avanzamento della consegna dei webhook

Ecco la query di monitoraggio che eseguo ogni cinque minuti. Produce un riepilogo dello stato di salute dell'intera pipeline di distribuzione, composto da una sola riga:

sql

SELECT
    -- Queue depth
    COUNT(*) FILTER (WHERE status = 'pending') AS pending,
    COUNT(*) FILTER (WHERE status = 'failed') AS awaiting_retry,
    COUNT(*) FILTER (WHERE status = 'in_flight') AS in_flight,
    COUNT(*) FILTER (WHERE status = 'dead_letter') AS dead_letter,
    
    -- Delivery rate (last hour)
    COUNT(*) FILTER (
        WHERE status = 'delivered' 
        AND delivered_at >= NOW() - INTERVAL '1 hour'
    ) AS delivered_last_hour,
    
    -- Failure rate (last hour)
    COUNT(*) FILTER (
        WHERE status IN ('failed', 'dead_letter')
        AND created_at >= NOW() - INTERVAL '1 hour'
    ) AS failed_last_hour,
    
    -- Oldest undelivered
    MIN(created_at) FILTER (
        WHERE status IN ('pending', 'failed')
    ) AS oldest_pending,
    
    -- Average delivery latency (last hour, successful only)
    AVG(last_response_ms) FILTER (
        WHERE status = 'delivered'
        AND delivered_at >= NOW() - INTERVAL '1 hour'
    ) AS avg_delivery_ms_last_hour

FROM webhook_queue;

Migliori oldest_pending Il valore è la metrica più importante in questa query. Se è più vecchio della finestra massima di tentativi (somma di tutti i ritardi di backoff), c'è qualcosa che non va a livello strutturale: il worker è bloccato, l'endpoint è irraggiungibile oppure la coda cresce più velocemente di quanto tu riesca a svuotarla.

Invio un avviso in base a tre condizioni: il conteggio delle lettere non funzionanti aumenta (gli endpoint non funzionano in modo permanente e nessuno sta indagando), l'età della coda in sospeso supera i 30 minuti (la consegna è in ritardo) e Latenza P95 per endpoint superiore alle soglie che indicano un'affidabilità di erogazione degradata Il terzo è il segnale di allarme precoce: la latenza aumenta prima che si verifichino guasti. Un endpoint che rispondeva in 200 ms e inizia a rispondere in 3 secondi sta per andare in timeout.

La coda delle lettere non consegnate non è solo un luogo di archiviazione

La maggior parte dei team implementa una coda di messaggi non recapitabili, ovvero una tabella in cui i webhook non riusciti vengono eliminati. La controllano occasionalmente durante la gestione degli incidenti. Questo è uno spreco.

La coda dei messaggi non recapitabili è il set di dati di debug più prezioso a tua disposizione. Ogni riga rappresenta un tentativo di consegna che il tuo sistema ha fallito più volte. Lo schema dei messaggi non recapitabili ti rivela informazioni che le metriche di successo non ti forniranno mai.

python

def analyze_dead_letters(cur, hours=24):
    """
    Analyze recent dead letter entries for patterns.
    Returns per-endpoint failure analysis.
    """
    cur.execute("""
        SELECT 
            endpoint_id,
            endpoint_url,
            COUNT(*) AS dead_count,
            
            -- Most common error
            MODE() WITHIN GROUP (ORDER BY last_error) AS primary_error,
            
            -- Most common status code
            MODE() WITHIN GROUP (ORDER BY last_status_code) 
                AS primary_status_code,
            
            -- Timing
            MIN(created_at) AS first_dead,
            MAX(created_at) AS last_dead,
            
            -- Average attempts before giving up
            AVG(attempt_count)::INTEGER AS avg_attempts
            
        FROM webhook_queue
        WHERE status = 'dead_letter'
          AND created_at >= NOW() - INTERVAL '%s hours'
        GROUP BY endpoint_id, endpoint_url
        ORDER BY dead_count DESC
        LIMIT 20
    """, (hours,))
    
    return cur.fetchall()

Quando esamino le lettere non recapitate, cerco tre schemi ricorrenti.

Guasti del cluster: 50 messaggi non recapitabili per lo stesso endpoint nella stessa ora significano che l'endpoint è andato in crash e non si è ripristinato entro la finestra di retry. Azione: estendere la finestra di retry o implementare una re-coda manuale.

Modelli di codici di stato: Un picco di errori 401/403 indica che le credenziali dell'endpoint sono state ruotate e che nessuno ha aggiornato la configurazione del webhook. Un picco di errori 429 (Troppe richieste) significa che si sta superando il limite di richieste e che è necessario limitare il traffico.

Accumulo graduale: 2-3 lettere morte al giorno per un singolo endpoint, distribuite uniformemente. Questo è lo schema più insidioso: l'endpoint funziona per lo più ma presenta guasti intermittenti che esauriscono i tentativi nel tempo. La soluzione di solito è in aumento max_attempts per quello specifico endpoint o riducendo il timeout.

Esecuzione del lavoratore

Il circuito principale che collega tutto:

python

import signal
import sys

running = True

def shutdown_handler(signum, frame):
    global running
    running = False
    print(f"Received signal {signum}, shutting down gracefully...")

signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)

def main():
    print("Webhook delivery worker starting...")
    
    while running:
        try:
            deliver_webhooks(batch_size=50)
        except Exception as e:
            print(f"Worker error: {e}")
            time.sleep(5)  # back off on errors
            continue
        
        # Update latency stats every 100 iterations
        # (cheap operation, doesn't need to run every loop)
        if int(time.time()) % 100 == 0:
            conn = get_connection()
            cur = conn.cursor(cursor_factory=RealDictCursor)
            try:
                cur.execute(
                    "SELECT DISTINCT endpoint_id FROM endpoint_health"
                )
                for row in cur.fetchall():
                    update_latency_percentiles(cur, row['endpoint_id'])
                conn.commit()
            finally:
                cur.close()
                conn.close()
        
        # Poll interval — 500ms keeps latency low without
        # hammering the database
        time.sleep(0.5)

    print("Worker shut down cleanly.")

if __name__ == '__main__':
    main()

Migliori SIGTERM Il gestore è essenziale per arresti puliti negli ambienti containerizzati. Quando Kubernetes invia SIGTERM, il worker termina il suo batch corrente, conferma la transazione ed esce. Senza questo, si ottengono righe bloccate in in_flight stato senza alcun lavoratore che li stia elaborando.

Cosa non fa questo sistema (e quando ne hai bisogno di più)

Questa implementazione gestisce fino a circa 10,000 consegne al minuto su una singola istanza PostgreSQL con 2-3 processi worker. Oltre tale limite, sono necessarie tre modifiche.

Innanzitutto, sostituisci la coda PostgreSQL con Redis Streams o RabbitMQ. SELECT FOR UPDATE SKIP LOCKED Questo schema crea contesa di scrittura sulla tabella della coda ad alti volumi di dati. Un message broker dedicato elimina questo problema.

In secondo luogo, aggiungete un limite di frequenza per ogni endpoint. Alcuni endpoint riceventi hanno limiti di frequenza (100 richieste al minuto, 1,000 all'ora). Senza un limite di frequenza lato client, supererete la loro quota e riceverete un errore 429. Implementate un token bucket per ogni endpoint.

In terzo luogo, aggiungete la firma della richiesta. Le firme HMAC-SHA256 sul payload consentono all'endpoint ricevente di verificare che il webhook provenga dal vostro sistema e non sia stato manomesso durante la trasmissione. Questo è un requisito fondamentale per qualsiasi sistema webhook che invii dati finanziari.

Il sistema descritto in questo articolo rappresenta il fondamento. Gestisce i problemi più complessi, come la logica di ritentativo, l'interruzione automatica dei circuiti, la misurazione della latenza e l'analisi dei messaggi non recapitati, elementi essenziali per qualsiasi sistema di consegna di webhook, indipendentemente dalle sue dimensioni. I componenti specifici da aggiungere (message broker, limitatore di velocità, firma delle richieste) dipendono dalle esigenze di throughput e sicurezza.

La parte più importante è quella che la maggior parte dei team trascura: misurare il sistema di distribuzione stesso. Se non si è in grado di rispondere alla domanda "qual è la latenza di consegna P95 verso l'Endpoint X nelle ultime 24 ore?", si sta operando alla cieca. Prima di tutto, create gli strumenti di misurazione. Tutto il resto verrà di conseguenza.

Domande frequenti sulla consegna tramite webhook

Il mittente di un webhook dovrebbe garantire la consegna "exactly-one"?

Di solito no. Il mittente dovrebbe rendere visibili i tentativi di ripetizione e fornire un ID evento stabile; il destinatario dovrebbe garantire l'idempotenza dell'elaborazione, in modo che un evento possa essere inviato più di una volta senza duplicare il risultato aziendale.

Quali tentativi falliti dovrebbero essere ripetuti?

Riprova solo gli errori che il tuo contratto classifica come temporanei, come errori di rete, timeout e risposte specifiche del server. Non riprovare ripetutamente richieste non valide, errori di autenticazione o altri errori permanenti senza un percorso di risoluzione esplicito.

Quando un team dovrebbe passare da una coda basata su database a una basata su un'altra?

È opportuno spostare la coda del database quando la contesa misurata, l'età del backlog, la velocità di trasmissione o le esigenze di ripristino operativo indicano che tale coda non soddisfa più i termini del contratto di fornitura. Le decisioni relative alla capacità devono basarsi sul carico di lavoro effettivo, non su una generica affermazione relativa alla frequenza delle richieste.

Articolo Precedente

I migliori software di tracciamento per affiliazioni iGaming nel 2026

Articolo successivo

L'intelligenza artificiale nel settore iGaming: casi d'uso di ChatGPT per i casinò

Cesare Fikson
Autore:

Cesare Fikson

Sono un analista di dati per l'iGaming, specializzato nell'analisi e nell'interpretazione dei dati relativi alle piattaforme di gioco online e alle attività di gioco d'azzardo, nonché alle tendenze di mercato. Analizzo il comportamento dei giocatori, le prestazioni di gioco e l'andamento dei ricavi per ottimizzare l'esperienza di gioco e le strategie aziendali.

Richiedi una demo
STEP 1 DI 3
Grazie, sei in coda.
Un tecnico di NowG ti contatterà entro un giorno lavorativo per programmare la tua dimostrazione.
Indice