All notable changes to rl-nginx will be documented here. Release candidates
remain preview software and must not be treated as ABI-stable versions.
Live production protocol smoke. make production-smoke builds nginx with
this module, starts it against the RateLimitly production fleet using only a
RATELIMITLY_AUTH_KEY, and drives real HTTP requests through the ordinary
pre-content admission path. It proves that a reported latency sample is
stored and read back by a guard on the same tracker, and that a one-token
bucket admits once and then rejects, checking each decision as an HTTP status
and a $ratelimitly_verdict so fail-open and fail-close can never look like
an authenticated decision. Discovery is derived from the credential alone.
See integration-tests/README.md.
The new production-smoke CI job is pinned to main, runs only for pushes
or a maintainer’s manual dispatch, and is the only job permitted to read a
secret; tests/test-ci-gates.py now keeps every required job secret-free and
red-case tests the live job’s actor pin, concurrency isolation, per-run
namespace, and step-level credential scope.
The module builds with clang again. ngx_http_rn_tracker() declared
buffer_size uninitialized and assigned tracker->buffer_size only when
buffer_size= was given. Clang cannot correlate that write with the separate
buffer_size_set flag, so auto/cc/clang’s -Wconditional-uninitialized
fired and nginx’s -Werror turned it into a hard build failure on every
clang host. The runtime fallback was never affected — tracker is
zeroed before the assignment and rn_build_tracker_values() resolves the
effective value from the API key’s latency_buffer_size_max — so the local
now carries that same zero default and the assignment is unconditional.
A clang-build CI job compiles both supported nginx lines with CC=clang,
because the existing jobs are GCC-only and GCC does not implement this
warning. tests/test-ci-gates.py red-case tests the job so it cannot quietly
stop using clang.
An IPv6 nameserver in /etc/resolv.conf no longer prevents nginx from
starting. Derived system DNS now passes IPv6 nameservers to nginx in the
bracketed form its address parser requires, so a host whose resolv.conf lists
an address such as fd7a:115c:a1e0::53 — common with Tailscale, IPv6-only,
and many dual-stack setups — no longer fails configuration with
invalid port in resolver and failed to create system DNS resolver for
ratelimitly. A nameserver nginx cannot parse, such as the zone-scoped
fe80::1%eth0, is now skipped with a warning instead of failing
configuration, and derived names are NUL-terminated for the resolver parser.
Setting ratelimitly_dns_resolver or an HTTP-scope resolver is no longer
required as a workaround.
API keys must use credential format 1. The locked C-client dependency is
now v1.0.0, which intentionally rejects legacy unversioned credentials and
unknown format versions. Reissue every API key before deploying this
rl-nginx revision; a legacy value fails nginx -t instead of reaching a
worker.
Latency tracker identity, guards, and reporting are now separate. Define
tracker state with ratelimitly_tracker, reference it from
ratelimitly_guard tracker=... threshold=..., and opt into exactly one
post-response sample with ratelimitly_report <tracker>. Guards no longer
report implicitly. A report can be used without a guard or admission rule,
and ratelimitly_report off suppresses inheritance.
ratelimitly_timeout is removed. There is no alias: a configuration that
still uses it fails nginx -t with unknown directive.
ratelimitly_timeout <T> made T the entire admission budget (it set the
client’s attempt timeout with zero retries). ratelimitly_policy replaces it
with a multi-round policy, so the migration is not a one-for-one textual
substitution:
| Old | Equivalent | Worst-case admission wait |
|---|---|---|
ratelimitly_timeout T; |
ratelimitly_policy single_round unit=T; |
T — unchanged |
ratelimitly_timeout T; |
ratelimitly_policy standard unit=T; |
3 * T — three units |
ratelimitly_timeout T; |
ratelimitly_policy standard unit=T/3; |
T — unchanged, using the new default shape |
Choose single_round to preserve the previous behaviour exactly, or
standard with a third of the old value to keep the same worst case while
adopting the replay/final-receive shape. Writing standard unit=T with the
old value triples the budget.
The derived policy horizon is now validated against the API key. A
unit= that was previously accepted can now be rejected at configuration
load when unit * (sum(replay_gap) + final_wait_units) exceeds the
credential’s dedup_ttl_ms_max. See
configuration.
ratelimitly_policy rejects duplicate occurrences. ratelimitly_timeout
was last-wins, so a base configuration plus an environment-specific override
include is no longer valid for this directive and fails nginx -t. This
matches ratelimitly_tenant, ratelimitly_auth_key, ratelimitly_bind and
ratelimitly_debug, which already rejected duplicates.
Latency-tracker identity now depends on the credential when buffer_size
is omitted. The effective buffer_size falls back to the API key’s
latency_buffer_size_max, and that value is part of the tracker ID. Rotating
to a credential whose quota differs re-identifies every guard that relies on
the fallback, discarding accumulated latency history. Set buffer_size
explicitly on each ratelimitly_tracker in any configuration expected to
survive a key rotation.
rl-c-client dependency to v1.0.0; resource
requests now inherit the client’s local enforcement of the API key’s
rate_window_size_ms_max before DNS, serialization, or UDP transmission;rl-c-client dependency to v0.6.0 and
allowed a ratelimitly rule to contain one or more latency guards without a
resource zone or group;rl-c-client dependency to v0.5.0 and
replaced ratelimitly_timeout with ratelimitly_policy: standard exposes
the locked three-unit default, single_round provides a literal one-round
policy, and custom exposes the complete request-policy parameter set;v0.5.0
requires valid SRV membership; andrestructured the public documentation around the RateLimitly operation model, concrete nginx examples, an explicit nginx/client ownership boundary, and version-locked links to the authoritative C-client documentation.
This dependency update changes the wire IDs of existing buckets and latency trackers. During a rolling upgrade, old and new workers therefore address separate server state until old resource windows and tracker TTLs expire.
rl-c-client dependency to v0.4.0, including
the oldest-trusted-server response strategy and retry behavior;Changes since v0.1.0-rc.1:
aarch64 static and relocated-dynamic coverage for both
supported nginx releases and extended sanitizers to both nginx lines;make check preserve and exercise its caller-selected static build,
separated contributor, release-only, and optional-private gate contracts,
and made dynamic relocation exercise six representative behavior groups;ratelimitly_report contract, including report-only and completed fail-open
work and suppression after denial, fail-close, or client abort;bucket= or
service= argument; value-only quotes are now rejected because nginx treats
them as literal identifier bytes;rl-c-client/main probes;$ratelimitly_verdict production warmup oracle;keep_port=false replaces the
source port as soon as safely possible and does not wait for an independent
fire-and-forget latency report, and made its ordering oracle compare
module-completion events instead of racing an external process observer; andThe first source-only public preview:
rl-c-client v0.2.0 release by tag and full commit;The planned preview scope and supported matrix are maintained in
docs/compatibility.md. Release notes will record the
exact source revisions, platform, compiler, and validation evidence when a
tagged release is made.