This document defines the observable request lifecycle and decision contract. Configuration syntax and wire-field construction are specified separately in Configuration DSL and Wire mapping. The meanings of a resource request and an independent latency report come from the locked C-client Operation Model.
The module runs as nginx’s final HTTP pre-content handler. nginx access control
and earlier pre-content routing or preparation handlers MUST complete before
the module sends a RateLimitly request. A request rejected or finalized before
that point MUST NOT consume a RateLimitly resource. Subrequests and requests
without an effective ratelimitly rule MUST continue without a RateLimitly
admission request. An independent ratelimitly_report does not turn such a
request into an admission request.
A protected main request that reaches the final admission point MUST expand all effective rules into one combined rate-limit check containing:
The combined check MAY contain zero resources when at least one guard remains. Such a request is still an admission decision: nginx proceeds only when every guard passes. The configuration DSL does not expose a request containing neither resources nor guards.
An nginx internal redirect remains part of the same main HTTP request. Once that request has obtained an admission result, later internal routing MUST reuse it and MUST NOT send another RateLimitly request. An admitted request therefore consumes once even when an index, error page, or another content handler internally redirects before producing the response. Subrequests remain unprotected by this module and MUST NOT create an independent admission.
The effective label, zone bucket/rate values, tracker service values, and guard
threshold values are rendered for the request. An empty bucket or service, a bucket or
service over 1024 bytes, a label over 256 bytes, another invalid dynamic value,
or a C-client request-construction error MUST follow the configured failure
policy. An empty label omits the label from the request. Internal nginx
allocation or event-registration failures MAY return 500 Internal Server
Error instead.
The worker-local UDP socket and C client are created lazily by the first
request with an effective admission rule or report handled by that worker. A running nginx worker therefore
proves process liveness, not that the worker has initialized RateLimitly
enforcement or completed discovery. Failed initialization retries use bounded
backoff; protected requests during that interval immediately follow
ratelimitly_fail.
Supported deployments MUST publish SRV records as required by
ratelimitly_dns_srv. The locked C client resolves
those records and their A/AAAA targets through ratelimitly_dns_resolver,
the HTTP-scope resolver, or system DNS (/etc/resolv.conf).
It sends the request to every currently usable discovered endpoint address.
The resolver is captured from the http context and applies process-wide to the
worker client. Server and location resolver overrides do not change RateLimitly discovery.
rl-nginx does not choose a single commit target and does not deduplicate resource consumption across those endpoints. A supported deployment MUST therefore provide a RateLimitly server topology and protocol contract in which that fan-out represents one logical admission/consumption. This commit-safety property is external to the nginx module. Operators MUST NOT assume there is a hidden single-target fallback when a discovered topology lacks that property.
The module passes the policy selected by ratelimitly_policy to the locked C
client. The default standard unit=20ms policy sends to all discovered
servers, prefers the oldest server’s valid response, performs one replay to
servers still missing a valid response, then waits through one final
receive-only unit. A selected allow or deny also triggers best-effort
completion delivery to servers still missing a response. Its maximum admission
wait and wire deduplication TTL are 3 * U, or 60ms by default.
single_round sends once, waits for at most one unit, and disables both replay
and completion delivery. custom exposes the replay-gap schedule, replay count,
final receive interval, and completion delivery. The exact syntax, horizon
formula, and validation rules are in Configuration DSL.
DNS refresh policy remains client-owned and is not configurable through rl-nginx.
Fan-out, response preference, replay scheduling, completion delivery, and TTL derivation are client-owned and are defined in the locked Resource-Request HA Policy. The paragraphs above specify the supported nginx selections and their externally visible effect.
The locked client requires SRV discovery. A failed, empty, or non-conforming
membership fails with RCLIENT_ERR_DNS; there is no direct tenant-name/UDP
port fallback. The client owns the corresponding
DNS Refresh
behavior.
A valid result MUST contain exactly as many guard and resource results as the request sent. A mismatch is a protocol/compatibility failure and follows the configured failure policy; partial response arrays MUST NOT be treated as a decision.
When status is successful and cardinality is exact, the module MUST admit the request to content processing only if all of the following are true:
success value is true;passed set; andtokens_deficit == 0.Otherwise the module MUST return 429 Too Many Requests. A valid RateLimitly
deny returns 429 under both failure policies. A valid allow represents
consumption of every requested resource, if any, and MUST advance directly to
nginx content processing without another pre-content handler. The admitted
client, content handler, or upstream can still fail afterward; such failures
do not reverse the consumption.
The module registers the nginx variable $ratelimitly_verdict. It MUST evaluate
to allow only after a valid result satisfies the complete allow contract
above, and to deny only after a valid result fails that contract. It MUST be
not found (rendered as - by normal nginx log formats) for an unprotected or
bypassed request, a request still awaiting a result, and every fail-open,
fail-close, internal-error, or aborted outcome.
This variable is a decision provenance signal, not the final HTTP status. An
allow can be followed by a content/upstream error, while a deny produces
429. Operations MAY include it in an access-log format to prove warmup
without enabling debug logging.
ratelimitly_fail applies when the module cannot obtain a valid, complete
decision:
| Condition | open |
close |
|---|---|---|
| Valid allow | Admit to content processing | Admit to content processing |
| Valid RateLimitly, guard, or resource deny | Return 429 |
Return 429 |
| No usable discovery target | Admit without a valid decision | Return 429 |
| UDP send failure or response timeout | Admit without a valid decision | Return 429 |
| Authentication/protocol failure with no valid decision | Admit without a valid decision | Return 429 |
| Response cardinality mismatch | Admit without a valid decision | Return 429 |
| Invalid dynamic request value or request-construction failure | Admit without a valid decision | Return 429 |
| Internal nginx allocation/event failure | MAY return 500 |
MAY return 500 |
Invalid or irrelevant datagrams can be discarded by the C client. If no valid response then arrives, the visible terminal result can be a timeout rather than the error associated with the discarded packet.
Rate checks MUST NOT block an nginx worker. The module starts an asynchronous borrowed-input C-client request, arms an nginx timer to the client deadline, and resumes HTTP phase processing from the completion callback.
The request-pool resources, guards, label, and callback context MUST remain valid until completion or cancellation. Client disconnect and nginx request cleanup MUST cancel the C-client request, remove the timer, and release the nginx and worker in-flight accounting exactly once. A synchronous callback from timeout handling can release the request pool; timeout code MUST NOT access the request context afterwards.
Worker shutdown MUST destroy the worker C client while its resolver adapter is still live, allowing outstanding resolver activity to be cancelled, and then close the worker socket. Reload and shutdown MUST leave no active request timer, resolver context, or UDP connection owned by an exited worker.
Rate and latency-report request headers set steering_feedback to zero. If a
valid response carries steering_feedback = 0, the locked client asks the
module to replace the worker’s UDP source port. The module MUST mark the rebind
pending and defer it until the worker has no in-flight RateLimitly requests.
It MUST NOT close or replace the socket from inside its active UDP read
callback. A zero-delay nginx event performs the rebind once both conditions
are safe. The event MUST register the replacement socket before retiring the
current socket. If replacement setup fails, the current socket remains active
and the module retries later; a failed steering rebind by itself MUST NOT turn
a usable worker into an outage.
A latency report is an independent fire-and-forget request. It is not related
to the rate request that admitted the HTTP operation and does not require that
request’s source port. The module MUST NOT hold a requested rebind for a future
latency report. After the C-client callback reports keep_port = false (wire
steering_feedback = 0), it SHOULD use the replacement source port for
subsequent sends as soon as nginx can safely replace the socket. A send
completed reentrantly while the feedback datagram’s UDP read callback is still
active MAY use the old port; otherwise later sends use the replacement
endpoint.
For a main request with an effective ratelimitly_report <tracker>, the nginx
log-phase handler MUST make at most one fire-and-forget latency-report attempt.
Reporting is explicit and independent: applying a guard never enables a
report, the report tracker need not be guarded, and a report-only location
does not send a Rate Request.
The module attempts the report after completed content when there was no admission rule, after a valid exact-cardinality allow, or after fail-open continuation. In all three cases nginx performed the work whose latency was requested. The final application or upstream HTTP status does not change that fact. A valid RateLimitly deny, fail-close outcome, internal nginx failure, or client connection error, timeout, destruction, or abort MUST NOT produce a report because the configured work was not completed for the client.
The observed duration starts at the nginx request timestamp and ends when the
log-phase handler runs. It is clamped to the inclusive unsigned 32-bit range
1..4294967295 milliseconds. The report contains exactly one entry derived
from the explicitly selected tracker. Report construction or send failure MUST
NOT change the HTTP result. Because reports are best effort, the first report
on a cold worker MAY fail while DNS discovery is still in progress; the module
does not delay the HTTP response to wait for membership.
The implemented observability surface is nginx access/error logging plus the documented module log markers. The module does not expose Prometheus counters, histograms, or a health endpoint. Exact marker interpretation and sensitive-log handling are defined in the operations guide.