Basin Pipelines: log Sume webhook deliveries and dedupe by job_id

Cloudflare Basin Pipelines streams ingest up to 1 GB/s. Log each signed Sume webhook delivery as one record and dedupe on job_id; Python sketch included.

4 min readSume
All posts

Cloudflare's October 1, 2026 changelog says Basin is generally available and that each Basin Pipelines stream can now ingest up to 1 GB/s, up from 5 MB/s. A Sume webhook receiver sends nothing close to that, so the useful part is the record: log each verified delivery with its event, job_id and request_id, and treat job_id as the dedupe key.

The facts used

The first row is from the Cloudflare changelog; the rest are from Sume's webhooks guide. I am not showing Cloudflare's ingestion API, because I did not read it.

Basin and Sume webhook facts (read 2026-10-03)
SourceFact
Cloudflare changelog, Oct 1, 2026Basin GA; each Basin Pipelines stream ingests up to 1 GB/s, up from 5 MB/s
Sume webhooksTerminal events only: job.completed, job.failed, job.canceled
Sume webhooksUp to 10 delivery attempts, 30 s apart by default, 10 s timeout per attempt
Sume webhooksUse job_id as the idempotency key on your side

One record per delivery

Verify the signature first. Sume signs the timestamp, a dot and the raw body with HMAC SHA 256, and sends it as sume-v1= in x-sume-webhook-signature; during a secret rotation the header holds several entries, and any match is valid. Reject an empty secret and a stale timestamp. Then build a record and drop repeats of the same job_id and event. Return a 2xx only after the record is stored.

The sketch keeps seen ids in memory; in production use your store's unique key.

import hashlib, hmac, json, time

SEEN: set[str] = set()

def handle(raw: bytes, ts: str, sig_header: str, secret: str):
    if not secret:
        raise ValueError('empty webhook secret')
    if abs(time.time() - int(ts)) > 300:
        return None
    want = 'sume-v1=' + hmac.new(secret.encode(), f'{ts}.'.encode() + raw, hashlib.sha256).hexdigest()
    if not any(hmac.compare_digest(e.strip(), want) for e in sig_header.split(',')):
        return None
    body = json.loads(raw)
    key = f"{body['job_id']}:{body['event']}"
    if key in SEEN:
        return None
    SEEN.add(key)
    return {'event': body['event'], 'job_id': body['job_id'],
            'request_id': body.get('request_id'), 'status': body.get('status'), 'received_at': int(time.time())}

if __name__ == '__main__':
    s, ts = 'test-secret', str(int(time.time()))
    raw = json.dumps({'event': 'job.completed', 'job_id': 'job_1', 'request_id': 'job_1', 'status': 'OK'}).encode()
    sig = 'sume-v1=' + hmac.new(s.encode(), f'{ts}.'.encode() + raw, hashlib.sha256).hexdigest()
    print(handle(raw, ts, sig, s))
    print(handle(raw, ts, sig, s))

Keep polling as a fallback

Ten refused attempts leave you with a failed delivery and a job that still finished. Sume says delivery is an optimization and not the only recovery path, so keep status_url polling for events that never arrive. The Redeliver action re-sends a job's real terminal event with a fresh signature and does not use up one of the automatic attempts.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume