This release closes the remaining horizontal-scaling gaps surfaced by the sizing & scaling work: graceful shutdown for zero-downtime rolling restarts, a rate limiter that shares state across replicas, and job processing that stays correct when more than one worker is running. It also adds actionable diagnostics to Stripe checkout. All changes are backward compatible — the new enforcement paths are opt-in.
Added: graceful connection draining on shutdown
The HTTP server now configures Jetty 12 GracefulHandler with a stop timeout, so shutdown drains in-flight requests instead of cutting them — enabling zero-downtime rolling restarts. On stop the server stops accepting new connections, rejects new requests with 503, and waits up to :drain-timeout-ms (default 30000, env HTTP_DRAIN_TIMEOUT_MS) for in-flight requests to finish before halting. An explicit nil disables draining.
Added: replica-shared HTTP rate limiting
A config-driven rate-limit interceptor is now part of the default HTTP pipeline. With an active Redis cache it enforces a fixed-window limit shared across all replicas; with no cache it falls back to a per-process counter (single-node only) and warns at startup. Enforcement is opt-in (default off) so a framework upgrade cannot start returning 429 to consumers. Limits are configurable per environment via :boundary/http :rate-limit and the HTTP_RATE_LIMIT / HTTP_RATE_LIMIT_WINDOW_MS env vars.
This release also closes a latent bug where a rejected request set the response but not :halt?, letting the downstream handler overwrite the 429 with a 200 — the limit was counted but never enforced.
Fixed: bounded in-memory rate limiter
The per-process fallback limiter only pruned a client’s own timestamps and never evicted stale client keys, so high-cardinality client ids (rotating API keys, many remote addresses) could grow process state without bound on a long-running node. The tracked-client map is now bounded by a hard cap (max-tracked-clients = 10000): stale clients are swept, and if still full the least-recently-active client is evicted, all atomically inside one swap!. The bundled prod/acc configs now default rate limiting to disabled — enable it together with an active Redis cache. dev stays enabled (single node, where a per-process counter is correct).
Fixed: exactly-once job processing across workers
Two multi-instance correctness gaps in the job system are closed:
-
Missing-handler re-enqueue. The handler registry is per-process, so an instance can dequeue a job type it has no handler for. Such a job is now re-enqueued (bounded by
:max-requeues, default 5) so an instance that owns the handler can pick it up, rather than being silently dead-lettered. Only once the budget is exhausted is it failed terminally with a clearNoHandlerError. A worker started with an empty registry warns loudly. -
Atomic scheduled-job claim. Promoting a due scheduled job is now an atomic claim — Redis uses
ZREM(only the worker whoseZREMreturns 1 enqueues), in-memory usesswap-vals!— so a scheduled job runs exactly once across N workers instead of being promoted by several at once.
Fixed: Stripe checkout diagnostics and blank-safe URLs
Two hardening fixes for boundary-payments:
-
create-checkout-sessionnow logs Stripe’s error type, code, param, and message on any non-2xx response, so a400is actionable from the logs instead of juststatus=%d id=%s. -
success_url/cancel_urlnow defer a blank (empty or whitespace) override to the redirect URL. Previously an empty override — e.g. an unsetPUBLIC_BASE_URLupstream — produced an emptysuccess_urlthat Stripe rejects with a400, because""is truthy.
Version alignment
All libraries bumped to v1.0.1-alpha-34 to maintain lockstep versioning.
Upgrade
Re-run the installer to pick up the latest release:
curl -fsSL https://get.boundary-app.org | bash
No migration required. The new rate-limit enforcement and graceful draining are opt-in and configurable per environment; existing deployments behave as before until enabled.