|
| 1 | +# Benchmarks |
| 2 | + |
| 3 | +Production benchmark results for hookflare running on Cloudflare Workers. |
| 4 | + |
| 5 | +## Environment |
| 6 | + |
| 7 | +- **Runtime**: Cloudflare Workers (production deployment) |
| 8 | +- **Region**: DFW (Dallas-Fort Worth edge) |
| 9 | +- **Plan**: Workers Free tier |
| 10 | +- **Tool**: [hey](https://github.com/rakyll/hey) + Python concurrent.futures |
| 11 | +- **Date**: 2026-03-16 |
| 12 | + |
| 13 | +## Webhook Ingress (Core Path) |
| 14 | + |
| 15 | +The ingress endpoint receives a webhook, verifies the signature (if configured), checks idempotency, and enqueues for delivery. Returns `202 Accepted`. |
| 16 | + |
| 17 | +### Throughput & Latency |
| 18 | + |
| 19 | +| Concurrency | Requests | RPS | Avg Latency | P50 | P95 | P99 | Error Rate | |
| 20 | +|---|---|---|---|---|---|---|---| |
| 21 | +| 10 | 50 | 25 | — | — | — | — | 0% | |
| 22 | +| 20 | 100 | 47 | — | — | — | — | 0% | |
| 23 | +| 50 | 200 | 128 | — | — | — | — | 0% | |
| 24 | +| 50 | 500 | **149** | **265ms** | **239ms** | **570ms** | **642ms** | **0%** | |
| 25 | + |
| 26 | +- 105 of the 500 requests returned `429 Rate Limited` (expected — default limit is 100 req/60s per source) |
| 27 | +- Zero `500 Internal Server Error` responses |
| 28 | + |
| 29 | +### Latency Distribution (500 requests, 50 concurrent) |
| 30 | + |
| 31 | +``` |
| 32 | + 10% in 20ms |
| 33 | + 25% in 170ms |
| 34 | + 50% in 239ms ← P50 |
| 35 | + 75% in 323ms |
| 36 | + 90% in 565ms |
| 37 | + 95% in 570ms |
| 38 | + 99% in 642ms |
| 39 | +``` |
| 40 | + |
| 41 | +### What the Ingress Path Does |
| 42 | + |
| 43 | +Each accepted request performs: |
| 44 | +1. Source lookup (in-memory cache, 60s TTL — zero D1 reads after cold start) |
| 45 | +2. Signature verification (HMAC-SHA256, timing-safe) |
| 46 | +3. Idempotency check (KV read, only if header present) |
| 47 | +4. Queue send (Cloudflare Queues) |
| 48 | + |
| 49 | +Heavy I/O (R2 payload archive, D1 event record) is deferred to the queue consumer. |
| 50 | + |
| 51 | +## Sequential Requests (Single Client) |
| 52 | + |
| 53 | +| Request | Latency | |
| 54 | +|---|---| |
| 55 | +| 1 | 310ms | |
| 56 | +| 2 | 480ms | |
| 57 | +| 3 | 311ms | |
| 58 | +| 4 | 398ms | |
| 59 | +| 5 | 343ms | |
| 60 | + |
| 61 | +Average: ~370ms per request for a single sequential client. |
| 62 | + |
| 63 | +## Health Check (Baseline) |
| 64 | + |
| 65 | +| Metric | Value | |
| 66 | +|---|---| |
| 67 | +| Avg Latency | 152ms | |
| 68 | +| Fastest | 34ms | |
| 69 | +| RPS | 130 | |
| 70 | + |
| 71 | +This includes network round-trip from the client to the nearest Cloudflare edge. The Worker itself executes in <1ms (simple JSON response). |
| 72 | + |
| 73 | +## Free Tier Capacity Estimate |
| 74 | + |
| 75 | +Based on Cloudflare free tier limits: |
| 76 | + |
| 77 | +| Resource | Free Limit | Per Event | Daily Capacity | |
| 78 | +|---|---|---|---| |
| 79 | +| Workers Requests | 100K/day | 1 (ingress) + 1 (consumer) | ~50K events | |
| 80 | +| D1 Reads | 5M/day | ~3 (source + subscriptions + dest) | ~1.6M events | |
| 81 | +| D1 Writes | 100K/day | ~2 (event + delivery) | ~50K events | |
| 82 | +| DO Requests | 100K/day | ~1 (delivery dispatch) | ~100K events | |
| 83 | +| Queue Messages | 1M/month | 1 | ~33K/day | |
| 84 | +| KV Reads | 100K/day | 0-1 (idempotency, if header present) | ~100K events | |
| 85 | +| R2 Class A Ops | 1M/month | 1 PUT (consumer) | ~33K/day | |
| 86 | + |
| 87 | +**Bottleneck: ~33K events/day on free tier** (Queue messages limit). |
| 88 | +**Paid plan ($5/mo)**: 10M Queue messages/month → ~330K events/day. |
| 89 | + |
| 90 | +## How to Run |
| 91 | + |
| 92 | +```bash |
| 93 | +# Deploy to your Cloudflare account |
| 94 | +cd packages/worker |
| 95 | +npx wrangler deploy |
| 96 | + |
| 97 | +# Bootstrap |
| 98 | +curl -X POST https://your-worker.workers.dev/api/v1/bootstrap \ |
| 99 | + -H "Content-Type: application/json" -d '{"name":"bench"}' |
| 100 | + |
| 101 | +# Create a source |
| 102 | +curl -X POST https://your-worker.workers.dev/api/v1/sources \ |
| 103 | + -H "Authorization: Bearer hf_sk_xxx" \ |
| 104 | + -H "Content-Type: application/json" \ |
| 105 | + -d '{"name":"bench-source"}' |
| 106 | + |
| 107 | +# Run benchmark |
| 108 | +hey -n 500 -c 50 -m POST \ |
| 109 | + -H "Content-Type: application/json" \ |
| 110 | + -d '{"type":"bench.test"}' \ |
| 111 | + https://your-worker.workers.dev/webhooks/<source_id> |
| 112 | +``` |
| 113 | + |
| 114 | +## Optimization History |
| 115 | + |
| 116 | +| Version | Avg Latency | RPS | Error Rate | Change | |
| 117 | +|---|---|---|---|---| |
| 118 | +| v0.1 (6 sequential I/O) | 860ms | 21 | 8-20% | Baseline | |
| 119 | +| v0.2 (deferred R2+D1) | 420ms | 38 | 20% | KV rate limiter still on hot path | |
| 120 | +| **v0.3 (in-memory rate limit)** | **265ms** | **149** | **0%** | **Current** | |
| 121 | + |
| 122 | +Key optimizations: |
| 123 | +- Source lookup cached in-memory (60s TTL) — eliminates D1 read per request |
| 124 | +- R2 write + D1 event creation deferred to queue consumer |
| 125 | +- KV-based rate limiter replaced with in-memory counter — eliminates KV write per request |
| 126 | +- Queue send + KV idempotency write run in parallel |
0 commit comments