Senast uppdaterad den 24 juni 2026 av Caesar Fikson
Direkt svar: En pålitlig webhook- eller affiliate-postback-leveranstjänst behöver en hållbar kö, begränsade återförsök med jitter, kretsbrytning per slutpunkt, dubblettskydd, signaturverifiering och telemetri som visar om leveransen saktar ner innan den misslyckas.
Denna implementering håller exekveringsmodellen avsiktligt liten: Python-workers och PostgreSQL för en kö, leveranshistorik och slutpunktshälsa. Det är en praktisk utgångspunkt för B2B SaaS, iGaming-affiliate-verksamhet och alla produkter som måste leverera utgående händelser utan att en enda HTTP-timeout behandlas som en förlorad konvertering.
Vad leveransavtalet måste garantera
| kontroll | Varför det spelar roll | Funktionskontroll |
|---|---|---|
| Hållbar händelsehistorik | Händelser överlever omstarter av arbetare. | Varje accepterad leverans har ett stabilt ID och en stabil status. |
| Undertecknad begäran | Mottagare kan verifiera avsändaren och upptäcka kroppsmanipulation. | Använd en tidsstämplad signatur och avvisa inaktuella förfrågningar. |
| Dubblettskydd | Omförsök kan annars skapa dubbletter av konverteringar eller uppdateringar. | Skicka ett händelse-ID och gör mottagaren idempotent. |
| Begränsade återförsök | Tillfälliga fel återställs utan att överbelasta en försämrad slutpunkt. | Backa vid jitter; stoppa efter en dokumenterad försöksgräns. |
| Slutpunktstelemetri | Enbart ködjupet döljer långsamma eller misslyckade partners. | Spåra framgångsfrekvens, äldsta väntande händelse och latensprocentiler per slutpunkt. |
Säkerhets- och dubblettkontroller kommer före återförsök
Behandla den utgående texten som känslig operativ data. Signera den exakta texten för begäran, inkludera en leveranstidsstämpel och ett oföränderligt händelse-ID, rotera signeringshemligheter och se till att den mottagande applikationen säkert ignorerar en uppspelning av samma händelse. Ett lyckat HTTP-svar är inte ett bevis på att en affärshändelse tillämpades exakt en gång; den mottagande sidan måste fatta det beslutet.
För ett arbetsflöde för postback för affiliate- eller iGaming-tjänster, håll klick-ID:n, konverterings-ID:n och utbetalningsrelevanta fält borta från loggar om inte åtkomstkontroller och lagringsregler är explicita. Den befintliga Scaleo-referensen nedan är endast relevant som ett exempel på latens för postback för affiliate-tjänster; den ersätter inte för att dokumentera leveransavtalet mellan dina egna tjänster.
Användbara implementeringsreferenser: Verifiering av Stripe webhook-signatur, PostgreSQL SELECT och SKIP LÅSToch AWS-vägledning om exponentiell backoff och jitter.
Datamodellen
Allt börjar med två tabeller: en för leveranskön och en för latensmätningarna.
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 saker att notera om detta schema.
Först den webhook_queue tabellen använder en next_attempt_at kolumn istället för en separat schemaläggningsmekanism. Arbetaren söker efter rader där status IN ('pending', 'failed') AND next_attempt_at <= NOW()Det här är en fattigmanskö och den fungerar bra upp till cirka 10 000 leveranser per minut. Utöver det, byt till en ordentlig meddelandeförmedlare.
Andra, endpoint_latency tabellen fungerar som en ringbuffert. Jag rensar regelbundet rader som är äldre än 24 timmar. Latensprocentilerna i endpoint_health beräknas från detta rullande fönster – de representerar aktuellt beteende, inte historiska medelvärden.
Tredje, endpoint_health tabellen implementerar brytarens tillståndsmaskin. Mer om detta nedan.
Leveransarbetaren
Kärnarbetarslingan är avsiktligt enkel. Komplexitet hör hemma i återförsökslogiken och kretsbrytaren, inte i själva leveransvägen.
pytonorm
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]}'
}
Ocuco-landskapet SELECT FOR UPDATE SKIP LOCKED klausulen är avgörande för att köra flera arbetarinstanser. Utan SKIP LOCKED, två arbetare skulle blockera på samma rad. Med den hämtar varje arbetare en annan batch av väntande webhooks. Detta ger dig horisontell skalning genom att helt enkelt starta fler arbetarprocesser.
Ocuco-landskapet time.monotonic() ring istället för time.time() är avsiktligt. time.time() kan hoppa bakåt under NTP-justeringar. time.monotonic() går aldrig bakåt, vilket är viktigt när du mäter latens på under en sekund.
Försök igen med logik med exponentiell backoff och jitter
När en leverans misslyckas avgör tidpunkten för återförsök om systemet återställer sig smidigt eller skapar en dånande flock som hamrar mot en kämpande slutpunkt.
pytonorm
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)
Varför fullt jitter istället för avkorrelaterat jitter eller lika stort jitter? AWS publicerade den definitiva analysen av detta. Fullt jitter (slumpmässig uppdelning mellan 0 och det exponentiella taket) ger den lägsta totala slutförandetiden för alla klienter. Lika stort jitter (slumpmässig uppdelning mellan halva taket och det fulla taket) är mer konservativt men långsammare för att dränera eftersläpningen av återförsök. För webhook-leverans där du har många oberoende slutpunkter är fullt jitter rätt val eftersom varje slutpunkts återförsök är oberoende – du koordinerar inte mellan dem.
Säkring: Sluta hamra på trasiga ändpunkter
Brytarmönstret förhindrar att ditt system slösar resurser på slutpunkter som ständigt misslyckas. Utan det ackumulerar en död slutpunkt hundratals väntande försök som alla får timeout på 15 sekunder vardera – vilket förbrukar din arbetskapacitet på leveranser som aldrig kommer att lyckas.
pytonorm
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,))
Half_open → open-eskaleringen med fördubblad nedkylningstid är den detalj som de flesta implementeringar missar. Om en slutpunkt misslyckas under proberingen (half_open-tillstånd) vill du inte försöka igen om ytterligare 5 minuter. Slutpunkten är fortfarande trasig. Fördubbla nedkylningstiden till 10 minuter, sedan 20, med en maxgräns på 1 timme. Detta förhindrar att kretsbrytaren blir en periodisk hamrande mekanism.
Latensprocentilspårning
Genomsnitten ljuger. En slutpunkt med en genomsnittlig svarstid på 200 ms kan svara på 50 ms i 95 % av fallen och 3 000 ms de övriga 5 %. Genomsnittet ser bra ut. P95 avslöjar ett problem som drabbar 1 av 20 leveranser.
pytonorm
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 är en ordnad aggregerad funktion som beräknar exakta percentiler. För stora datamängder skulle du byta till percentile_disc (vilket returnerar ett faktiskt observerat värde snarare än interpolering) eller använd en t-digest-approximation. För webhook-leverans med ett 24-timmarsfönster är exakta percentiler på rådata tillräckligt snabba upp till cirka 100 000 mätningar per slutpunkt.
Ocuco-landskapet get_slow_endpoints Funktionen är vad jag kör som en schemalagd kontroll var 15:e minut. Ändpunkter med P95 över 2 sekunder flaggas för undersökning. Ändpunkter med P95 över 5 sekunder får sin brytartröskel reducerad — de tillåts färre fel i följd innan kretsen öppnas, eftersom varje misslyckad leverans binder upp en arbetstråd under hela timeout-tiden.
Övervakning av Webhooks leveranshälsa
Här är övervakningsfrågan jag kör var femte minut. Den producerar en hälsosammanfattning på en enda rad för hela leveranspipelinen:
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;
Ocuco-landskapet oldest_pending värdet är det viktigaste måttet i den här frågan. Om det är äldre än ditt maximala återförsöksfönster (summan av alla backoff-fördröjningar) är något strukturellt fel – antingen har arbetaren fastnat, slutpunkten är blockerad eller så växer kön snabbare än du kan tömma den.
Jag varnar under tre omständigheter: antalet oanvända brev ökar (slutpunkterna misslyckas permanent och ingen undersöker), väntande kölängd överstiger 30 minuter (leveransen halkar efter), och P95-latens per slutpunkt överskrider tröskelvärden som indikerar försämrad leveranstillförlitlighetDen tredje är den tidiga varningssignalen – latensen ökar innan fel gör det. En slutpunkt som svarade på 200 ms och börjar svara på 3 sekunder är på väg att börja få en tidsgräns.
Kön för döda brev är inte bara lagring
De flesta team implementerar en kö med oanvända tecken som en tabell där misslyckade webhooks hamnar för att dö. De kontrollerar den då och då under incidentrespons. Detta är slöseri.
Kön för döda tecken är din mest värdefulla felsökningsdatauppsättning. Varje rad representerar en leverans som ditt system försökt flera gånger och gett upp. Mönstret av döda tecken berättar saker som framgångsmåtten aldrig kommer att göra.
pytonorm
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()
När jag granskar döda bokstäver letar jag efter tre mönster.
Klusterfel: 50 tomma tecken för samma slutpunkt under samma timme innebär att slutpunkten slutade fungera och inte återställdes inom återförsöksfönstret. Åtgärd: förläng återförsöksfönstret eller implementera manuell omköning.
Statuskodmönster: En topp i 401/403 tomma tecken betyder att slutpunkten roterade autentiseringsuppgifterna och att ingen uppdaterade webhook-konfigurationen. En topp i 429 (För många förfrågningar) betyder att du överskrider deras hastighetsgräns och behöver begränsa den.
Gradvis ackumulering: 2–3 döda tecken per dag för en enda slutpunkt, jämnt fördelade. Detta är det mest osäkra mönstret – slutpunkten fungerar mestadels men har intermittenta fel som uttömmer återförsök över tid. Åtgärden ökar vanligtvis. max_attempts för den specifika slutpunkten eller att minska timeouten.
Köra arbetaren
Den huvudsakliga loopen som binder ihop allting:
pytonorm
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()
Ocuco-landskapet SIGTERM hanteraren är avgörande för rena avstängningar i containeriserade miljöer. När Kubernetes skickar SIGTERM slutför arbetaren sin aktuella batch, genomför transaktionen och avslutar. Utan detta får du rader som fastnar i in_flight status utan att någon arbetare bearbetar dem.
Vad det här systemet inte gör (och när du behöver mer)
Denna implementering hanterar upp till cirka 10 000 leveranser per minut på en enda PostgreSQL-instans med 2–3 arbetsprocesser. Utöver det behövs tre ändringar.
Först, ersätt PostgreSQL-kön med Redis Streams eller RabbitMQ. SELECT FOR UPDATE SKIP LOCKED Mönstret skapar skrivkonflikter i kötabellen vid hög dataflöde. En dedikerad meddelandehanteringsmäklare eliminerar detta.
För det andra, lägg till en hastighetsbegränsning per slutpunkt. Vissa mottagande slutpunkter har hastighetsgränser (100 förfrågningar per minut, 1 000 per timme). Utan hastighetsbegränsning på klientsidan kommer du att förbruka deras kvot och få 429 förfrågningar. Implementera en token-bucket per slutpunkt.
För det tredje, lägg till förfrågningssignering. HMAC-SHA256-signaturer på nyttolasten låter den mottagande slutpunkten verifiera att webhooken kom från ditt system och inte manipulerades under överföringen. Detta är tabellinsatser för alla webhook-system som skickar finansiell data.
Systemet i den här artikeln är grunden. Det hanterar de svåra problemen – logik för återförsök, kretsbrytning, latensmätning, analys av döda brev – som varje webhook-leveranssystem behöver oavsett skala. De specifika komponenterna du bygger på (meddelandemäklare, hastighetsbegränsare, förfrågningssignering) beror på dina dataflödes- och säkerhetskrav.
Den del som är viktigast är den del som de flesta team hoppar över: att mäta själva leveranssystemet. Om du inte kan svara på "vad är P95-leveransfördröjningen till slutpunkt X under de senaste 24 timmarna" arbetar du i blindo. Bygg instrumenteringen först. Allt annat följer.
Vanliga frågor om webhook-leverans
Bör en webhook-avsändare lova exakt en gångs leverans?
Vanligtvis nej. En avsändare bör göra omförsök synliga och tillhandahålla ett stabilt händelse-ID; mottagaren bör göra bearbetningen idempotent så att en händelse kan levereras mer än en gång utan att affärsresultatet dupliceras.
Vilka misslyckanden bör försökas igen?
Försök endast igen fel som ditt kontrakt klassificerar som tillfälliga, såsom nätverksfel, timeouts och valda serversvar. Försök inte upprepade gånger igen med felaktigt formaterade förfrågningar, autentiseringsfel eller andra permanenta fel utan en uttrycklig åtgärdsväg.
När bör ett team gå bortom en databasbaserad kö?
Flytta när uppmätt konkurrens, orderstockens ålder, dataflöde eller operativa återställningsbehov visar att databanskön inte längre uppfyller leveransavtalet. Kapacitetsbeslut bör följa observerad arbetsbelastning, inte ett generiskt anspråk på begäranfrekvens.