Hookway

Performance results and methodology

Measured on 2026-10-01, using a local Docker deployment of the V2 ingress runtime. Two Hookway replicas, each limited to 2 vCPU and 4 GiB, passed three 5,000 requests/s, 20-second bursts and a continuous 500 requests/s, 30-minute mixed workload against a synthetic destination. These are measured results for the recorded build and workloads, not an AWS capacity claim or a production availability guarantee.

Recorded build and environment

Application bindings per replica were GOMAXPROCS=2, GOGC=100, GOMEMLIMIT=3GiB, 256 executing requests, 6,144 bounded waiting handlers and a four-second admission wait. Publication timeout was five seconds; the client and body-read timeouts were 30 seconds, and the HTTP write timeout was 35 seconds. These are explicit small-webhook test settings. The default waiting queue is disabled; these settings are not recommended sizing for maximum-size documents.

The measured signed and ordinary bodies were 6,532 bytes. Timestamped signatures were refreshed by the generator, preserving the configured five-minute validity window. Repeated event identities make this a publication-operation workload, not a test of distinct FIFO messages, deduplication or consumer delivery.

Burst results

Each repetition planned and dispatched 100,000 signed requests over 20 seconds through nginx. Every request returned its expected HTTP 200 before five seconds, and acknowledged publication counts matched the destination receipts.

RepetitionOffered rateTimely successes / plannedHighest driver p99 histogram boundMean consumed CPU cores per replicaLargest sampled application memory
15,000/s100,000 / 100,000500 ms1.28 / 1.30212 MiB
25,000/s100,000 / 100,0001,000 ms1.34 / 1.36304 MiB
35,000/s100,000 / 100,0001,000 ms1.62 / 1.65395 MiB

Across these three bursts: 300,000 / 300,000 timely successes, with zero 503s, transport errors, publication errors, generator skips or over-budget responses. One replica was not required to handle the whole 5,000/s workload. Six before/after echo calibrations each dispatched and passed 100,000 requests.

Continuous mixed workload

One uninterrupted 30-minute run offered 500 requests/s across three paths. All 900,000 / 900,000 planned requests completed with expected HTTP 200 responses below five seconds. Publication counts matched all 900,000 destination receipts; routing lookups matched 298,800.

PathRequests/sTimely successes / plannedp50 / p95 / p99 histogram upper bounds
Fixed publication167300,600 / 300,60025 / 25 / 25 ms
Timestamped signature167300,600 / 300,60025 / 25 / 25 ms
Lookup then publication166298,800 / 298,80025 / 50 / 50 ms

Each replica averaged about 0.25 consumed CPU cores. Sampled container memory peaks were 60.13 and 73.93 MiB. Executing and waiting admission gauges drained to zero, and goroutines settled at 20 per replica in the post-run idle observation. This was a continuous run, not an aggregation of restarted short segments.

The archived soak proxy counted two extra upstream observations that could not be classified retrospectively. They are excluded from the workload denominator. Generator responses and application/destination publication counts reconcile exactly. A subsequent 5,000-request check retained method/path access records and reconciled every generated request without unclassified observations.

Additional workload checks

On the same application image and two-replica allocation:

The transparent JSON relay has a wire-size cap, not a decoded base64 file-size validator. Its upstream owns the decoded-document limit. Raw/XML/multipart oversize checks returned 413 before forwarding. PDF/DOCX-shaped fixtures test transport, not document format validation.

Methodology and interpretation

  1. Record the exact application image, configuration, payload sizes and allocation of every role. Use realistic neutral payloads and independent signing/body-digest expectations.
  2. Calibrate the generator, proxy and synthetic destination before and after load at the offered rate. Monitor their CPU, memory and OOM state separately from Hookway; fast echo calibration alone cannot qualify a slow-backend test.
  3. Schedule load by time, independently of response completion. Keep every planned request in the denominator. A missed dispatch, capacity skip, wrong response, transport error or response taking at least five seconds fails the timely-success gate. Use a 30-second per-request timeout.
  4. Reconcile generator counts with per-replica publication metrics and independently counted destination receipts. For batch traffic, reconcile both parent requests and child publication operations.
  5. Repeat bursts and run an uninterrupted sustained workload. Sample CPU, throttling, memory, heap, goroutines and admission gauges during load and after drain. Resource samples do not establish exact peaks; histogram bounds do not establish exact percentiles.
  6. Preserve failed trials. Earlier trials exposed admission refusal logging amplification, canonicalization cost, and generator/proxy resource limits. A fallback trial that skipped 5,044 planned requests failed; increasing only generator outstanding capacity produced the three full fallback passes.

The signed-handler component benchmark fell from approximately 181–184 µs/op and 227 KB allocated/op to 91–96 µs/op and 113 KB allocated/op on the recorded host. The native path proves when JSON bytes already match the JavaScript representation and avoids constructing a VM for those inputs; other inputs fall back to JavaScript. This component comparison does not predict an equal end-to-end throughput improvement. Reusable broker clients/connections and bounded, nonblocking overload logging address separate sources of contention.

These finite samples observed 100% timely success; they do not establish a 99.99% or 99.999% production SLA. They also do not establish real AWS durability, FIFO ordering/deduplication, SNS filtering, TLS behavior, autoscaler response, a workday-long leak-free lifetime, or 5,000/s bursts mixed with maximum-size files. The deployment must validate its actual traffic mix and downstream behavior.

Repeating measurements

Use an existing load generator and load balancer against your deployment; no performance-specific deployment stack or load client is shipped. Use fixtures whose repetition is safe, and report planned/completed work, dispatch skips and latency failures according to the methodology above. Go hot-path benchmarks remain available for profiling:

go test ./internal/httpapi -run '^
  

 -bench BenchmarkRepresentativeSignedFlow -benchmem
go test ./internal/jsoncanon -run '^
  

 -bench . -benchmem

The original raw traces, neutral fixtures and helper sources are retained as local evidence outside source control. This report summarizes them; it is not an archived runnable benchmark suite. Collect a fresh baseline for a new build or changed destination, workload, hardware, proxy or admission configuration.

Replay capture and ingest benchmarks

This is a separate historical replay sample, not part of the ingress workload or build recorded above. Keep its environment and sizing limits with the results.

Measured with make bench-replay on an Apple M4 Pro (arm64, 12 cores), Go 1.25.4, PostgreSQL 16 in a local container — single-process measurements to size a deployment, not throughput claims, from one run each. Core's figures are the request path of a skip_failed flow with one of two destinations down and a raw body (BenchmarkCaptureOverhead, 200 requests each; p99 is the 198th-fastest); replay's are ingests of one snapshot and one payload of the given size each (BenchmarkIngest, 30 records each, one ingester and GOMAXPROCS ingesters).

PayloadCore p99 without captureCore p99 with captureRecord uploadedReplay ingest, 1 ingester12 ingesters
1 KiB0.29 ms0.46 msfull~480 records/s~175 records/s
256 KiB3.3 ms4.0 msfull~135 records/s~145 records/s
4 MiB49 ms46 msfull~12 records/s~12 records/s
16 MiB177 ms175 msmetadata-only (too_large)~3 records/s~3.5 records/s

Capture adds bookkeeping, not work, to a request: the failure is recorded and the response is written; the record is built and uploaded afterwards on background workers, so above small payloads the difference is run-to-run noise. A failure whose retained bodies alone exceed the 64 MiB upload buffer — here a 16 MiB raw body, whose envelope copies and base64 form add up past it — is uploaded as its metadata-only too_large record instead of being held.

Replay ingest is serialized by the usage row every ingest updates: more ingesters (or more replicas) do not raise it, and at small sizes contention lowers it. The figures are therefore limits for the whole cluster, bounded by writing the artifacts to PostgreSQL (roughly 70–115 MB/s here, per-transaction latency at 1 KiB); size retention and the database for the capture rate you expect.

Shared protection benchmarks (2026-10-04)

Run on the same Apple M1 Pro/macOS host with Go 1.26.8, Docker Redis 7 and PostgreSQL 17. The declared profile expands each variant to 2,000 operations, with one or four workers, one hot identity or distinct identities, generous quotas, ten-connection pools and a two-second test deadline. Any backend error fails the benchmark. These are local samples; no production throughput threshold was asserted. Some frontend tests ran concurrently on the host.

HOOKWAY_REQUIRE_INTEGRATION=1 go test ./internal/protectionstore -run '^
  

 \
  -bench '^BenchmarkProtection
  

 -benchtime=2000x -count=1
HOOKWAY_REQUIRE_INTEGRATION=1 go test ./internal/protectionstore -run '^
  

 \
  -bench '^BenchmarkProtectionMaintenance
  

 -benchtime=1x -count=1
BackendStrategyIdentitiesWorkersElapsed/op msp95 msp99 msGo allocation bytes/op
memorytoken_buckethot_key10.00290.00450.00691714
memorytoken_buckethot_key40.00380.01910.03781716
memorytoken_bucketmany_keys10.00290.00480.00671975
memorytoken_bucketmany_keys40.00370.01980.04871978
memorysliding_window_counterhot_key10.00240.00410.00661762
memorysliding_window_counterhot_key40.00330.01750.02801763
memorysliding_window_countermany_keys10.00260.00340.00712023
memorysliding_window_countermany_keys40.00350.02100.05912026
redistoken_buckethot_key10.40430.70912.42102875
redistoken_buckethot_key40.11590.68051.04402989
redistoken_bucketmany_keys10.24040.33020.40352885
redistoken_bucketmany_keys40.10180.57110.75703000
redissliding_window_counterhot_key10.23220.48210.64732923
redissliding_window_counterhot_key40.09010.66540.83473035
redissliding_window_countermany_keys10.23900.31740.68702932
redissliding_window_countermany_keys40.10740.61660.74903044
postgrestoken_buckethot_key11.64992.38503.80908231
postgrestoken_buckethot_key41.24495.85707.19208362
postgrestoken_bucketmany_keys12.09262.61603.52208111
postgrestoken_bucketmany_keys41.92949.533013.43008262
postgressliding_window_counterhot_key11.50091.81801.97908239
postgressliding_window_counterhot_key41.28416.733011.41008383
postgressliding_window_countermany_keys11.99472.69503.77108137
postgressliding_window_countermany_keys41.73638.935012.47008289

In this run PostgreSQL p99 ranged from 1.979–13.43ms; Redis from 0.4035–2.421ms. A smaller preliminary sample had much longer PostgreSQL tails; host contention varies, and local runs cannot establish universal capacity. Admission wall-time throughput includes concurrent workers; it is not individual latency. PostgreSQL four-worker variants reported connection-acquisition wait means of 7.4–28.1 microseconds/op, alongside their serialized policy updates. Go allocation bytes/op are temporary allocations, not retained heap, process RSS or a prediction of a full production fleet's memory requirement.

The repeated 1,000-identity maintenance sample measured:

BackendOperationElapsed msBackend bytes after operationWAL bytes/op
Redispolicy update13.506657352 key bytesnot measured
Rediscleanup (100 rows)1.124588552 key bytesnot measured
PostgreSQLpolicy update317.140974848 table bytes888776
PostgreSQLcleanup (100 rows)5.3041384448 table bytes11472

A single maintenance observation does not estimate tail latency or sustained autovacuum/disk growth. Policy update remains an explicit maintenance operation; its total runtime can exceed a request's admission deadline. Table growth after cleanup reflects retained pages/dead tuples, not extended logical protection. Physical reclamation and vacuum need production monitoring.

These measurements predate later hardening changes. They do not size a current production deployment, and the earlier ingress results use a different workload.

PostgreSQL coordinates capacity and updates with a policy lock, so decisions for different identities within a shared policy also serialize. The production store deadline defaults to 250ms; benchmark operations use two seconds and fail on any backend error. Select policy boundaries and deadlines from deployment measurements. Memory, Redis and PostgreSQL remain supported choices. Redis key bytes and PostgreSQL table/WAL bytes are different measurements, not equivalent memory costs.