Due team che sviluppano sulla stessa API di URL shortener spesso finiscono con architetture di integrazione completamente diverse. Un team vuole un endpoint webhook che reagisca 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 design validi, ma solo uno funziona oggi per i click su Elido: i webhook per singolo click sono pianificati, non ancora disponibili, quindi il percorso operativo per i dati di click è il polling. 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/workspaces/{workspace_id}/analytics/clicks/recent o /v1/workspaces/{workspace_id}/analytics/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 al tuo endpoint HTTPS. Il tuo ricevitore lo gestisce, restituisce un 2xx e la consegna viene registrata come avvenuta con successo. Oggi questi eventi riguardano link e domini (link.created, link.updated, link.expired, domain.verified e altri ancora). Un evento click.created per singolo click è pianificato e non viene ancora emesso, quindi per ora nessun webhook scatta quando qualcuno fa click.
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.
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. Per i dati di click, leggi questa sezione come il piano per quando click.created sarà disponibile; fino ad allora, gli stessi casi d'uso usano un polling a intervalli brevi di clicks/recent.
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.created e lo espone tramite una connessione WebSocket o SSE al frontend sarà l'architettura che raggiungerà questo risultato senza martellare l'API di analisi, una volta disponibile l'evento. Oggi, un polling di un minuto su clicks/recent che si ferma all'ultima riga già vista è il massimo che si può ottenere.
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 sarà lo strumento corretto quando gli eventi per singolo click saranno disponibili; fino ad allora, mantieni breve l'intervallo di polling.
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.created pianificato dovrebbe portare abbastanza dati su cui agire immediatamente, senza una query round-trip. I workflow attivati da modifiche ai link, come link.created o link.expired, possono usare i webhook già oggi.
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.
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-Elido-Signature e, come alias, in X-Webhook-Signature. Il timestamp viene inviato separatamente in X-Webhook-Timestamp.
Devi verificare questa firma prima di elaborare il payload; la guida alla verifica delle firme webhook illustra anche Python, Go e la rotazione dei segreti. 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 === "link.updated") {
// Handle the link event
console.log("link updated:", 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
Elido riprova le consegne fallite con uno schema di backoff: il primo retry arriva 5 minuti dopo un errore, il secondo 15 minuti dopo. Una consegna ha in totale 3 tentativi per impostazione predefinita. Dopo 3 tentativi falliti la consegna viene contrassegnata come permanentemente fallita e appare nel log delle consegne dell'endpoint.
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 20 minuti (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.
Una volta disponibile click.created, 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. Fino ad allora, il percorso rapido è un polling a intervalli brevi di clicks/recent, e il passaggio di riconciliazione qui sotto funziona nello stesso modo.
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
Il polling è il punto di partenza di ogni integrazione di click oggi e continuerà a funzionare anche dopo la disponibilità dei webhook per singolo click. Ecco un'implementazione minimale che usa la libreria schedule e chiama clicks/recent su un intervallo di cinque minuti (la guida all'API di analisi dei link illustra in dettaglio i parametri cursor, data e fuso orario):
import schedule
import time
import requests
import os
API_BASE = "https://api.elido.app/v1"
WORKSPACE_ID = os.environ["ELIDO_WORKSPACE_ID"]
API_KEY = os.environ["ELIDO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Newest click already processed, kept across runs. clicks/recent returns
# newest first and next_cursor pages to older rows, so each run reads from
# the top and stops when it reaches this row.
_last_seen = None
def poll_recent_clicks():
global _last_seen
params = {"limit": 100}
newest = None
done = False
while not done:
resp = requests.get(
f"{API_BASE}/workspaces/{WORKSPACE_ID}/analytics/clicks/recent",
headers=HEADERS,
params=params,
timeout=10,
)
resp.raise_for_status()
body = resp.json()
for click in body.get("items", []):
key = (click["ts"], click["link_id"])
if key == _last_seen:
done = True
break
if newest is None:
newest = key
process_click(click)
next_cursor = body.get("next_cursor")
if not next_cursor:
break
params["cursor"] = next_cursor
if newest:
_last_seen = newest
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 tier di redirect di Elido filtra i click dei bot prima che vengano registrati. Le richieste di Googlebot, Bingbot, Slackbot, monitor di uptime, curl, librerie di scripting e User-Agent vuoti non vengono conteggiate come click e non appaiono nei risultati dell'API di analisi, né produrranno eventi click.created quando saranno disponibili.
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
- Perché usiamo ClickHouse per le analisi dei click (e non Postgres)
- Tracciamento GA4 server-side tramite il tier di redirect
- Raggiungere p95 < 15ms per i redirect da FRA, ASH e SGP
- Attribuzione cookieless spiegata: cosa funziona ancora nel 2026
- Verifica delle firme webhook: HMAC-SHA256 in Node, Python e Go
- API di analisi dei link: estrarre le statistiche dei click con una chiave API
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