Elido
12 min di letturaIntegrazioni

Webhook vs polling per il click tracking - scegli il pattern giusto

Un'analisi pratica di quando usare i webhook e quando fare polling dell'API di analisi per i dati di click: i costi nascosti di ciascun approccio, esempi di codice concreti in TypeScript e Python, e il pattern ibrido che copre la maggior parte dei casi d'uso in produzione.

Sasha Ehrlich
Compliance · EU residency
Split diagram comparing webhooks and polling for click tracking: left panel shows server pushing click.recorded events to receiver endpoint; right panel shows cron hitting /analytics/summary every N minutes

Due team che sviluppano sulla stessa API di URL shortener spesso finiscono con architetture di integrazione completamente diverse. Un team configura un endpoint webhook e reagisce a ogni click in tempo reale. L'altro scrive un cron job che esegue il polling dell'API di analisi ogni cinque minuti. Entrambi sono validi. La scelta tra di essi ha conseguenze reali per la latenza, il carico operativo e come il sistema si degrada quando qualcosa va storto.

Questo post illustra i reali compromessi.

I due pattern#

Polling#

Il polling significa che il tuo codice chiede all'API i dati di click recenti secondo una pianificazione. Un cron job si sveglia, chiama /v1/analytics/workspaces/{id}/clicks/recent o /v1/analytics/workspaces/{id}/summary, elabora i risultati, poi si mette in pausa fino al prossimo intervallo.

Il flusso di dati è pull-based: la tua infrastruttura avvia ogni interazione. Il server API non conosce i tuoi sistemi interni - risponde semplicemente alle query che invii.

Webhook#

I webhook significano che il server di Elido invia un evento click.recorded al tuo endpoint HTTPS poco dopo che un click viene elaborato. Il tuo ricevitore lo gestisce, restituisce un 2xx e la consegna viene registrata come avvenuta con successo.

Il flusso di dati è push-based: la piattaforma avvia il contatto. Il tuo endpoint deve essere raggiungibile da internet, deve avere TLS e deve rispondere in modo affidabile.

Diagramma affiancato: il polling ha il tuo cron job che invia GET all'API di Elido e recupera righe ogni cinque minuti; i webhook hanno il server di Elido che invia un POST click.recorded al tuo endpoint HTTPS che restituisce 2xx.

Quando il polling è la scelta giusta#

Il polling si adatta a un insieme specifico di condizioni. Se la maggior parte di questi si applica alla tua situazione, inizia con il polling e ricorri ai webhook solo quando un problema concreto ti obbliga.

Controlli entrambi i lati dell'integrazione. Quando il consumer è un dashboard o uno strumento di reporting che possiedi e gestisci, il polling ti dà un comportamento prevedibile e delimitato. Decidi tu l'intervallo; decidi tu la finestra temporale; decidi tu come gestire i risultati parziali.

Il tuo caso d'uso è retrospettivo. I report di campagna settimanali, i lavori di aggregazione mensili e le pipeline di riconciliazione non traggono vantaggio da una latenza inferiore al minuto. Un cron job che gira ogni ora contro /summary o /breakdown/country è architetturalmente più semplice e più facile da ragionare rispetto a un ricevitore webhook stateful con gestione dei retry.

Non hai un endpoint pubblico da esporre. I webhook richiedono un URL raggiungibile dall'infrastruttura di Elido. Se la tua integrazione gira all'interno di una rete privata, una funzione Lambda senza URL stabile o la macchina locale di uno sviluppatore, configurare un endpoint HTTPS in entrata potrebbe costare più complessità operativa di quanto valga il beneficio di latenza.

Il volume è basso. Con qualche migliaio di click al giorno, la differenza tra tempo reale e un ritardo di cinque minuti è raramente visibile agli utenti finali. Il polling è semplice da capire, semplice da debuggare e non produce sorprese infrastrutturali.

Quando i webhook sono la scelta giusta#

I webhook hanno senso quando la latenza è un requisito di prodotto piuttosto che un optional.

Stai costruendo un contatore live o una UX in tempo reale. Se il tuo prodotto mostra agli utenti un conteggio di click che si aggiorna visibilmente entro secondi da un redirect, il polling a qualsiasi intervallo ragionevole sembrerà notevolmente obsoleto. Un handler webhook che incrementa un contatore Redis sugli eventi click.recorded e lo espone tramite una connessione WebSocket o SSE al frontend è l'architettura che raggiunge questo risultato senza martellare l'API di analisi.

Stai arricchendo i record CRM per click. Collegare un evento di click a un record di contatto - identificare quale specifico prospect ha seguito il link nella tua email in uscita e aggiornare la timeline del suo CRM - è sensibile al tempo. Quando il polling recupera il dato cinque minuti dopo, il venditore potrebbe aver già chiamato. Un handler webhook che attiva un aggiornamento CRM entro secondi dal click è lo strumento corretto.

Stai gestendo workflow event-driven. I workflow attivati da eventi di click - inviare un'email di follow-up quando un link viene cliccato, aggiornare il segmento di un iscritto, decrementare un conteggio di inventario - sono consumatori naturali di webhook. L'evento click.recorded porta abbastanza dati su cui agire immediatamente, senza una query round-trip.

Hai un endpoint HTTPS stabile e pubblicamente raggiungibile. Questo è il prerequisito da cui dipende tutto il resto. Se hai già un'infrastruttura di produzione che accetta webhook in entrata da altri provider (Stripe, GitHub, Twilio), aggiungere Elido allo stesso ricevitore è a basso attrito.

I costi nascosti dei webhook#

I webhook sembrano semplici: il server invia un POST, tu lo gestisci. La superficie di implementazione reale è più ampia.

Diagramma a strati dei quattro gate che ogni consegna click.recorded in entrata attraversa: verifica della firma HMAC, finestra di replay di 300 secondi, deduplicazione idempotente sull'ID di consegna e disponibilita prima di restituire 2xx.

Verifica della firma#

Elido firma ogni consegna di webhook con HMAC-SHA256. Il formato della firma è v1=HMAC-SHA256(secret, "${unix_timestamp}.${body}"), consegnato nell'header X-Webhook-Signature. Il timestamp viene inviato separatamente in X-Webhook-Timestamp. Entrambi sono prodotti in services/webhook-dispatcher/internal/signing/hmac.go.

Devi verificare questa firma prima di elaborare il payload. Un ricevitore che salta la verifica elaborerà qualsiasi POST che raggiunge l'endpoint, incluse le richieste contraffatte da chiunque scopra il tuo URL webhook.

Ecco un handler Express minimale in TypeScript che verifica la firma prima di fare qualsiasi cosa con il payload:

import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();

// Use raw body middleware - JSON parsers consume the stream before you can hash it
app.use("/webhook", express.raw({ type: "application/json" }));

function verifySignature(
  secret: string,
  signature: string,
  timestamp: string,
  rawBody: Buffer,
): boolean {
  const message = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected =
    "v1=" + crypto.createHmac("sha256", secret).update(message).digest("hex");
  // Use timingSafeEqual to prevent timing-based enumeration
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8"),
  );
}

app.post("/webhook", (req: Request, res: Response) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const timestamp = req.headers["x-webhook-timestamp"] as string;

  if (!signature || !timestamp) {
    return res.status(400).json({ error: "missing signature headers" });
  }

  // Reject payloads older than 5 minutes
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    return res.status(400).json({ error: "payload too old" });
  }

  if (
    !verifySignature(
      process.env.WEBHOOK_SECRET!,
      signature,
      timestamp,
      req.body as Buffer,
    )
  ) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse((req.body as Buffer).toString("utf8"));

  if (event.type === "click.recorded") {
    // Handle the click event
    console.log("click recorded:", event.data);
  }

  // Always return 2xx promptly - do heavy processing async
  return res.status(200).json({ received: true });
});

La finestra di replay#

Il controllo del timestamp nell'esempio sopra applica quella che la documentazione di Elido chiama finestra di replay. Senza di essa, un attaccante che cattura un singolo payload valido firmato può riprodurlo indefinitamente - la firma rimane valida per sempre perché è calcolata da un timestamp fisso. Con il controllo, un payload più vecchio di cinque minuti viene rifiutato indipendentemente dalla validità della firma.

Imposta la tolleranza su qualcosa che la tua infrastruttura può gestire. Cinque minuti è il default convenzionale e corrisponde a quello usato da Stripe. Se il tuo ricevitore va occasionalmente offline per qualche minuto durante i deployment, questa finestra gli dà il tempo di tornare operativo e processare comunque le consegne in volo.

Retry e idempotenza#

Il webhook-dispatcher di Elido riprova le consegne fallite con uno schema di backoff: primo retry a 1 minuto, secondo a 5 minuti, terzo a 15 minuti. Il valore massimo di tentativi per consegna è 3 per impostazione predefinita, come definito nello schema webhook_deliveries. Dopo 3 tentativi falliti la consegna viene contrassegnata come permanentemente fallita e appare nel dashboard delle notifiche.

Ciò significa che il tuo ricevitore potrebbe ricevere lo stesso evento più di una volta. Qualsiasi elaborazione che ha effetti collaterali - scrittura su un database, invio di un'email, aggiornamento di un contatore - deve essere idempotente. L'header X-Webhook-Delivery porta un ID di consegna stabile che puoi usare come chiave di idempotenza.

// Before processing, check whether this delivery has already been handled
const deliveryId = req.headers["x-webhook-delivery"] as string;
const alreadyProcessed = await redis.get(`webhook:delivery:${deliveryId}`);
if (alreadyProcessed) {
  return res.status(200).json({ received: true, duplicate: true });
}
// Mark as processed with a TTL that covers the retry window
await redis.set(`webhook:delivery:${deliveryId}`, "1", "EX", 3600);

Il tuo endpoint deve essere altamente disponibile#

La finestra di retry è limitata. Se il tuo ricevitore è inattivo per più di circa 21 minuti (1 + 5 + 15), le consegne esauriranno i loro tentativi e falliranno definitivamente. Per gli eventi in cui la consegna garantita è importante - hook CRM, hook di fatturazione - la tua infrastruttura del ricevitore necessita di disponibilità adeguata, non di un server hobbistico che riavvia occasionalmente.

Questo è il costo più sottovalutato dei webhook per i team alle prime armi con l'HTTP in entrata. Il polling si degrada gradualmente: se il job di polling fallisce, si riesegue semplicemente al prossimo intervallo e recupera. Un ricevitore webhook non disponibile perde eventi definitivamente a meno che tu non abbia una strategia di riconciliazione.

I costi nascosti del polling#

Il polling sembra semplice dall'esterno. I costi reali si accumulano in produzione.

La latenza è il vincolo determinante. Un cron job che gira ogni cinque minuti significa che i dati di click sono obsoleti fino a cinque minuti. Per la maggior parte dei casi d'uso retrospettivi questo è accettabile; per qualsiasi cosa orientata all'utente, non lo è. Accorciare l'intervallo aiuta ma non elimina la latenza, e intervalli molto brevi (sotto il minuto) cominciano ad assomigliare a martellamento API piuttosto che a polling.

Richieste sprecate. La maggior parte degli intervalli di polling restituisce gli stessi dati della richiesta precedente. Se stai eseguendo il polling di un link a basso traffico ogni minuto e i click arrivano a circa uno all'ora, 59 su ogni 60 richieste non restituiscono nulla di nuovo. Queste richieste contano comunque ai fini del tuo limite di frequenza API.

Limiti di frequenza. L'API di Elido applica limiti di frequenza per workspace dimensionati in base al tier di fatturazione. Un job di polling che gira frequentemente su molti link in un workspace grande può raggiungere questi limiti, in particolare se altre automazioni nello stesso workspace stanno anche loro effettuando chiamate API. L'API restituisce 429 Too Many Requests con un header X-RateLimit-Scope: workspace quando questo accade.

Paginazione ed eventi persi. L'endpoint /clicks/recent usa la paginazione basata su cursore. Se esegui il polling su una finestra temporale fissa - ?from=<last_poll>&to=<now> - e il volume in quella finestra supera la dimensione della pagina, perderai eventi a meno che non segua next_cursor attraverso tutte le pagine. Un'implementazione di polling che non gestisce la paginazione perderà silenziosamente dati sotto carico.

Il pattern ibrido#

Per la maggior parte dei casi d'uso in produzione, la risposta migliore non è né l'uno né l'altro.

Diagramma dell'architettura ibrida: Elido invia gli eventi di click a un handler webhook come percorso primario in tempo reale, mentre un polling notturno di /summary e /timeseries riconcilia i totali nel tuo store e colma le lacune lasciate dalla finestra di retry.

Usa i webhook come percorso primario per la reazione in tempo reale: aggiornamenti CRM, contatori live, workflow event-driven. La latenza è bassa; il carico operativo è gestibile se hai già un'infrastruttura HTTPS in entrata.

Usa il polling come passaggio di riconciliazione settimanale o giornaliero: estrai la serie temporale completa per la settimana precedente, confronta i totali con ciò che il tuo handler webhook ha registrato e identifica eventuali lacune. Questo cattura le consegne che hanno esaurito la finestra di retry durante un'interruzione, gli eventi che sono arrivati fuori ordine e qualsiasi discrepanza tra il tuo stato locale e la fonte di verità di Elido.

L'API di analisi è adatta a questo ruolo. L'endpoint /summary restituisce i totali aggregati per un intervallo di date in un'unica query; l'endpoint /timeseries restituisce bucket giornalieri. Un job di riconciliazione che gira una volta per notte e confronta i conteggi di click registrati nel tuo CRM con il riepilogo dell'API per la stessa finestra può far emergere problemi di integrità dei dati prima che diventino problemi visibili ai clienti.

Un cron di polling in Python#

Per i team che vogliono iniziare con il polling e passare ai webhook in seguito, ecco un'implementazione minimale che usa la libreria schedule e chiama /clicks/recent su un intervallo di cinque minuti:

import schedule
import time
import requests
import os

API_BASE = "https://api.elido.app/v1/analytics"
WORKSPACE_ID = os.environ["ELIDO_WORKSPACE_ID"]
API_KEY = os.environ["ELIDO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Track the cursor across poll intervals so we only fetch new clicks
_cursor = None

def poll_recent_clicks():
    global _cursor
    params = {"limit": 100}
    if _cursor:
        params["cursor"] = _cursor

    while True:
        resp = requests.get(
            f"{API_BASE}/workspaces/{WORKSPACE_ID}/clicks/recent",
            headers=HEADERS,
            params=params,
            timeout=10,
        )
        resp.raise_for_status()
        body = resp.json()

        items = body.get("items", [])
        for click in items:
            process_click(click)

        next_cursor = body.get("next_cursor")
        if not next_cursor:
            # Persist the current cursor for the next run
            if items:
                _cursor = None  # reset: next poll fetches from now
            break
        params["cursor"] = next_cursor

def process_click(click: dict):
    # Replace with your actual processing logic
    print(f"click: link={click['link_id']} country={click.get('country_code')}")

schedule.every(5).minutes.do(poll_recent_clicks)

if __name__ == "__main__":
    poll_recent_clicks()  # run once on startup to catch up
    while True:
        schedule.run_pending()
        time.sleep(10)

In un deployment di produzione, sostituisci il print con il tuo sink effettivo - una scrittura su database, una chiamata API CRM, una pubblicazione su una coda di messaggi - e aggiungi la gestione degli errori con backoff esponenziale intorno alla chiamata requests.get.

Filtraggio dei bot e cosa significa per la tua integrazione#

Un dettaglio che riguarda entrambi i pattern: il servizio edge-redirect di Elido filtra i click dei bot prima di emettere gli eventi di click alla pipeline di elaborazione. Le richieste di Googlebot, Bingbot, Slackbot, monitor di uptime, curl, librerie di scripting e User-Agent vuoti non producono eventi click.recorded e non appaiono nei risultati dell'API di analisi.

Questo è importante perché significa che il tuo handler webhook o job di polling sta lavorando con conteggi di redirect umani, non conteggi di richieste HTTP grezze. Se stai correlando i dati di click di Elido con metriche lato server - i log del server della tua applicazione, i log di accesso di una CDN - aspettati che i numeri di Elido siano più bassi. La discrepanza non è un bug; è il filtro bot che rimuove il rumore prima che raggiunga te.

Per maggiori dettagli su cosa copre il filtro bot e come lo scorer del sospetto contrassegna il traffico borderline, la guida alle analisi ha una panoramica completa. Per le proprietà di sicurezza dello schema di firma dei webhook - incluso il formato HMAC, il binding del timestamp e cosa previene - vedi la security checklist.


La pagina dei prezzi ha la suddivisione di quali tier di piano includono gli endpoint webhook e a quali limiti di volume di consegna.

Correlati sul blog#

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Gratis, nessuna registrazione richiesta · 2 al giorno

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

Tag
click tracking
webhooks
url shortener api
link analytics
api integration
event-driven
polling
real-time analytics

Continua a leggere