r-client specification
Edit on GitHub →The normative specification every RateLimitly client implements. Client libraries and their API references live on the docs index.
On this page
- 1. Overview
- 2. Protocol Basics
- 2.1. Transport
- 2.2. Message Structure
- 2.3. Data Alignment
- 3. Connection Management
- 3.1. Server Discovery
- 3.2. Communication
- 4. Authentication
- 4.1. No Authentication (None)
- 4.2. Cookie Authentication
- 4.3. AES-256-GCM Authentication
- 5. Data Structures
- 5.1. Tenant Header
- 5.2. Guard Block (Request)
- 5.3. Resource Block (Request)
- 5.4. Service Latency Block (Request)
- 6. Recommended Client Usage State Machine
- 6.1. Transport states
- 6.2. Logical operation states
- 6.3. Event ordering and exactly-once completion
- 6.4. Error taxonomy
- 7. Client Operations
- 7.1. Rate Limiting (checkRateLimit)
- 7.2. Latency Reporting (reportLatency)
- 8. Error Handling
- 9. High Availability
- 9.1. server_id Decoding
- 9.2. Recommended Ranking Rule
- 9.3. Parameterized HA Resource-Request Strategy
- 9.4. DNS Refresh Is Separate
r-client Specification
1. Overview
This document specifies the behavior of an r-client, a client designed to communicate with an r-server (ratelimitly-server) for distributed rate limiting and load shedding. The specification is language-agnostic and provides a blueprint for developing r-client implementations in various programming languages.
The r-client communicates with the r-server over UDP, sending requests and receiving responses according to the Ratelimitly Wire Protocol.
2. Protocol Basics
2.1. Transport
All communication between the r-client and r-server occurs over UDP. Each message (request or response) is sent as a single UDP datagram.
2.2. Message Structure
Messages are composed of a sequence of records:
- Tenant Header (TLV): Identifies the tenant and contains request metadata.
- Auth Header (TLV): Contains authentication information.
- Protocol Data Unit (PDU): Represents the specific operation (e.g., rate request).
All multi-byte integer values are encoded in little-endian format.
2.3. Data Alignment
The protocol is designed for high performance and zero-copy parsing. Data structures are aligned to 4 or 8-byte boundaries. Implementations should be mindful of this, especially when creating and parsing packets.
3. Connection Management
3.1. Server Discovery
The r-client discovers server instances through DNS resolution of a tenant-specific domain name. The discovery process is as follows:
-
SRV Records: The client resolves
_ratelimitly._udp.<tenant_dns_name>. The SRV records provide the authoritative list of server target hostnames and ports. - Target Address Resolution: For each SRV target hostname returned, the client resolves that target via A/AAAA to obtain usable socket addresses.
- Explicit Configuration: The client can be explicitly configured with a static list of server addresses, bypassing DNS discovery.
DNS SRV Setup
To enable SRV discovery, publish SRV records under _ratelimitly._udp.<tenant_dns_name>. Each SRV record points at a target hostname and port; publish A/AAAA records for those targets. Direct A/AAAA records on <tenant_dns_name> do not replace the SRV layer.
The first DNS label of each SRV target hostname MUST encode the wire-protocol server_id in decimal as s-<server_id_decimal>. The server_id extracted from DNS MUST equal the server_id later returned by that server in the response Tenant Header.key_id.
Example target form:
s-1015809.rl1.example.com.
Example (BIND-style):
; tenant_dns_name = ratelimitly.example.com
_ratelimitly._udp.ratelimitly.example.com. 60 IN SRV 0 0 29292 s-1015809.rl1.example.com.
_ratelimitly._udp.ratelimitly.example.com. 60 IN SRV 0 0 29293 s-1048578.rl2.example.com.
s-1015809.rl1.example.com. 60 IN A 203.0.113.10
s-1048578.rl2.example.com. 60 IN A 203.0.113.11
Provider examples (fields and names may vary by UI):
Route 53 (SRV record):
-
Service:
_ratelimitly -
Protocol:
_udp -
Name:
ratelimitly.example.com -
Priority/Weight/Port/Target:
0 / 0 / 29292 / s-1015809.rl1.example.com
Cloudflare (SRV record):
-
Type:
SRV -
Name:
_ratelimitly._udp -
Priority/Weight/Port/Target:
0 / 0 / 29292 / s-1015809.rl1.example.com
GCP Cloud DNS (SRV record):
-
Name:
_ratelimitly._udp.ratelimitly.example.com. -
Priority/Weight/Port/Target:
0 / 0 / 29292 / s-1015809.rl1.example.com.
Namecheap (SRV record):
-
Service:
_ratelimitly -
Protocol:
_udp -
Host:
ratelimitly.example.com -
Priority/Weight/Port/Target:
0 / 0 / 29292 / s-1015809.rl1.example.com
3.2. Communication
The client sends UDP packets to one or more discovered server addresses. For operations that expect a response (like a rate request), the client should be prepared to receive a response from any of the servers it sent the request to.
4. Authentication
The r-client supports three authentication methods, specified in the Auth Header.
4.1. No Authentication (None)
-
tlv_type:0x414E - No authentication is performed. This is suitable for trusted environments or testing.
4.2. Cookie Authentication
-
tlv_type:0x4143 - The client sends a 32-byte cookie hash as raw binary data.
-
Cookie credentials are provisioned as a Bech32 key (
rl-cookie...) that embeds:-
auth_method=cookie -
key_id(tenant ID) - 32-byte cookie hash payload
-
4.3. AES-256-GCM Authentication
-
tlv_type:0x4541 - This is the most secure method, providing authenticated encryption.
- The PDU is encrypted using AES-256-GCM.
-
The
Auth Headercontains a 12-byte nonce and a 16-byte authentication tag. - Encryption scope is the PDU bytes only; Tenant TLV and Auth TLV remain plaintext.
-
AAD is
tenant_tlv || auth_tlv_prefix, whereauth_tlv_prefix = tlv_type || tlv_size || nonce. - Nonce MUST be unique per AES key.
-
AES credentials are provisioned as a Bech32 key (
rl-aes...) that embeds:-
auth_method=aes -
key_id(tenant ID) - 32-byte AES key payload
-
5. Data Structures
The following are language-agnostic definitions of the core data structures used in the protocol.
5.1. Tenant Header
| Field | Size (bytes) | Type | Description |
|---|---|---|---|
tlv_type |
2 | Integer |
0x4C52 (“RL”) |
tlv_size |
2 | Integer |
40 |
key_id |
8 | Integer | Tenant identifier. |
unique_id |
16 | Binary | Unique request ID (e.g., UUIDv4). |
time_stamp |
8 | Integer | Milliseconds since UNIX epoch. |
steering_feedback |
1 | Boolean |
0 = change port, 1 = keep port. |
tenant_mgmt_flag |
1 | Boolean |
0 = regular operation, 1 = admin. Client is 0. |
padding |
2 | Binary | MUST be zero. |
5.2. Guard Block (Request)
| Field | Size (bytes) | Type | Description |
|---|---|---|---|
latency_tracker_id |
16 | Binary | Canonical content-defined latency-tracker ID. |
ttl_ms |
4 | Integer | Time-to-live for latency samples. |
max_samples |
4 | Integer | Max samples for latency tracking. |
buffer_size |
4 | Integer | Circular buffer size for latency tracking. |
min_sample_threshold |
4 | Integer | Minimum insertion rate for reliability. |
latency_threshold |
4 | Integer | Maximum acceptable latency in ms. |
current_latency |
4 | Integer | MUST be 0 in requests. |
Before building a guard block, a conforming client MUST derive
latency_tracker_id from the application-defined latency-tracker name,
ttl_ms, max_samples, buffer_size, and min_sample_threshold by the
canonical algorithm in
wire_protocol.md.
A latency-tracker name identifies one logical latency signal within the tenant. It is not an r-server name or necessarily a DNS or network-service name. Guards and reports intended to share observations MUST use the same name and final effective tracker configuration.
latency_threshold is a query over the tracker and is not part of its
identity. Different requests may therefore use different thresholds against
the same tracker.
5.3. Resource Block (Request)
| Field | Size (bytes) | Type | Description |
|---|---|---|---|
bucket_id |
16 | Binary | Canonical content-defined resource identifier. |
window_size_ms |
4 |
u32 |
Rate-limiting window in milliseconds. |
rate_limit |
4 |
u32 |
Maximum tokens allowed in the window. |
tokens_requested |
2 | Integer | Number of tokens requested. |
padding |
2 | Integer | MUST be zero. |
Before building a resource block, a conforming client MUST derive bucket_id
from the logical bucket name, window_size_ms, and rate_limit by the
canonical algorithm in
wire_protocol.md.
This makes each resource definition address independent tenant-local rate
state without prior server-side registration.
tokens_requested is operation-specific consumption and is not part of the
resource identity. Clients that intend to share rate state MUST use the same
logical bucket name, window, and limit consistently.
The response reuses rate_limit for actual consumption; it is not a new
configured limit and MUST NOT be used to derive a future bucket_id.
An r-server uses the received bucket_id directly. While the corresponding
state is live, it verifies that the accompanying window and limit match the
values that established the state. A mismatch is a malformed request, not a
rate denial: it consumes no tokens, produces no response, and is observed by
the client as request failure.
5.4. Service Latency Block (Request)
The service latency block is 36 bytes on the wire.
| Field | Size (bytes) | Type | Description |
|---|---|---|---|
latency_tracker_id |
16 | Binary | Canonical content-defined latency-tracker ID. |
ttl_ms |
4 | Integer | Time-to-live for latency samples. |
max_samples |
4 | Integer | Max samples for latency tracking. |
buffer_size |
4 | Integer | Circular buffer size for latency tracking. |
min_sample_threshold |
4 | Integer | Minimum insertion rate for reliability. |
observed_latency |
4 | Integer | Client-observed latency in ms. |
The report MUST derive latency_tracker_id from the same name and tracker
configuration used by its corresponding guards. observed_latency is an
individual sample and is not part of the identity.
An r-server uses the received latency_tracker_id directly. While the
corresponding state is live, it verifies that all four tracker configuration
values match the definition that established the state. A mismatch makes the
containing PDU malformed and causes no tracker or resource-state mutation.
6. Recommended Client Usage State Machine
This section describes one recommended client-side usage pattern. It is not a
wire-protocol requirement, and clients in Rust, C, Java, and JavaScript may
choose other policies while preserving the semantics defined by
wire_protocol.md.
The state machine below applies to both single-server and multi-server
membership snapshots. Section 9 defines the parameterized fan-out, replay, and
response-selection policy implemented by the MVP C client. Its design history
and remaining study questions are documented in
r-client-ha-design.md.
6.1. Transport states
The transport state is:
stateDiagram-v2
[*] --> Uninitialized
Uninitialized --> Discovering: start / DNS discovery required
Uninitialized --> Ready: start / explicit endpoints usable
Discovering --> Ready: usable endpoint set installed
Discovering --> Unavailable: discovery failure
Unavailable --> Discovering: retry timer
Ready --> RebindPending: steering keep_port=false
RebindPending --> Rebinding: no conflicting in-flight rate send
Rebinding --> Ready: replacement socket registered
Rebinding --> RebindPending: replacement failed
Ready --> Closing: close
Discovering --> Closing: close
Unavailable --> Closing: close
RebindPending --> Closing: close
Rebinding --> Closing: close
Closing --> Closed: socket, timers, and discovery released
Closed --> [*]
Transport states have these meanings:
| State | Meaning |
|---|---|
Uninitialized |
No endpoint set or client socket has been installed. |
Discovering |
DNS or explicit-endpoint validation is in progress. No operation may be reported as successful solely because discovery has started. |
Ready |
At least one usable server address and a usable UDP source socket exist. |
Unavailable |
No usable endpoint currently exists; the DNS refresh configuration may schedule discovery. |
RebindPending |
A trusted response requested keep_port = false; the current socket remains usable until replacement succeeds. |
Rebinding |
A replacement socket is being opened and registered transactionally. |
Closing |
New operations are rejected and existing operations are completed according to the API’s cancellation policy. |
Closed |
All sockets, timers, callbacks, and owned operation state have been released. |
The following transport properties are recommended for this usage pattern:
- A failed discovery or rebind should not destroy a currently usable endpoint.
- A replacement socket should be opened and registered before the old socket is closed.
- A rebind should wait for the safety condition defined by the host integration (normally no current in-flight rate send and no active receive callback). It should not wait for a later latency report.
-
After
keep_port = false, subsequent sends should use the replacement source port as soon as the safety condition permits. A reentrant send that is already executing MAY use the old port. -
Closing should be idempotent. No callback or timer should start a new operation after
Closingbegins.
6.2. Logical operation states
Each resource request operation has its own immutable operation_id (the wire
Tenant Header.unique_id), membership snapshot, logical request contents,
current transmission round, absolute deadlines, response set, selected
candidate, and terminal outcome. The operation state is independent of the UDP
socket and survives a transactional source-port replacement.
stateDiagram-v2
[*] --> Created
Created --> Prepared: validate and encode
Created --> Terminal: validation failure
Prepared --> Terminal: no usable endpoint
Prepared --> TransmissionRound: initial fan-out
TransmissionRound --> Terminal: oldest response or preference selection
TransmissionRound --> TransmissionRound: silent deadline / replay remains
TransmissionRound --> FinalReceive: last round silent / final interval enabled
TransmissionRound --> Terminal: last round silent / no final interval
FinalReceive --> Terminal: selected response or final timeout
TransmissionRound --> Cancelled: caller cancellation / owner teardown
FinalReceive --> Cancelled: caller cancellation / owner teardown
Terminal --> [*]
Cancelled --> [*]
An attempted wire transmission is not an admission decision. A rate request is not successful until the policy selects a valid response. Latency reporting uses the separate fire-and-forget transitions in Section 6.2.2.
6.2.1. Rate-request transition table
| Current state | Event | Required guard/action | Next state |
|---|---|---|---|
Created |
API call | Allocate one operation ID, snapshot the current server membership, and capture immutable request inputs. |
Prepared |
Created |
Invalid input | Return a local validation error; send nothing. |
Terminal |
Prepared |
Endpoint unavailable | Apply the configured failure policy; do not invent an allow response. |
Terminal |
Prepared |
Initial send succeeds | Send the same logical request to every server in the membership snapshot and arm the first absolute deadline. |
TransmissionRound |
TransmissionRound |
Valid correlated response | Record the responding server and update the oldest candidate. Select immediately if it is the globally oldest server or the preference deadline has elapsed; otherwise re-arm to the preference deadline. |
Terminal(Allowed/Denied) or TransmissionRound |
TransmissionRound |
Invalid or unrelated datagram | Do not add it to the response set or candidate; remain able to receive a later valid response. |
TransmissionRound |
TransmissionRound |
Preference deadline with candidate | Select the oldest valid candidate. |
Terminal(Allowed/Denied) |
TransmissionRound |
Preference deadline without candidate | Continue waiting until the round replay deadline. |
TransmissionRound |
TransmissionRound |
Replay deadline without candidate and replay remains | Reuse the same operation ID and replay only to snapshot servers from which no valid response has arrived. |
TransmissionRound |
TransmissionRound |
Last replay deadline without candidate | Enter the configured final receive-only interval, or report timeout when it is disabled. |
FinalReceive or Terminal(Failed) |
FinalReceive |
Valid correlated response | Apply the final preference rule; with a zero final preference, select the first valid response immediately. |
Terminal(Allowed/Denied) or FinalReceive |
FinalReceive |
Final deadline without candidate | Report timeout. |
Terminal(Failed) |
TransmissionRound or FinalReceive |
Cancellation/close | Suppress later completion and release operation ownership exactly once. |
Cancelled |
An accepted response is one that passes authentication, framing, correlation, and response-semantic checks. A response that fails a check is not a denial; it is invalid and must not complete the operation. Section 9.3 adds the trusted server-identity and response-selection checks.
The terminal rate outcomes are:
-
Allowed: every guard and resource condition grants the request; -
Denied: the server returned a valid response whose guard or resource conditions reject the request; -
Failed: the client could not obtain a valid response or local validation failed; the host integration applies its fail-open/fail-closed policy; and -
Cancelled: the caller or owning host request ended before a terminal server decision was accepted.
Only Allowed and Denied are server decisions. A client should keep
Failed and Cancelled distinguishable internally even if a host integration
maps both to the same HTTP or application result.
6.2.2. Latency-report transition table
| Current state | Event | Required guard/action | Next state |
|---|---|---|---|
Created |
Latency-report API call | Validate and build a report with the observed latency and service blocks. No resource request is required. |
Prepared |
Created |
Invalid input | Return a local validation error; send nothing. |
Terminal |
Prepared |
Endpoint unavailable | Record a local send failure; do not affect the completed rate outcome. |
Terminal |
Prepared |
Datagrams transmitted | Send once to every server currently known to the client. |
Reported |
Reported |
Any later event | No response is expected and no rate operation is reopened. |
Reported |
Latency reporting is best-effort, fire-and-forget, and independent of resource requests. A client may send only latency reports and never issue a resource request. A higher-level admission workflow may choose to report the protected work only after an allow decision, but that coupling belongs to the workflow, not to the latency-report protocol or core client operation.
6.3. Event ordering and exactly-once completion
An implementation should serialize state transitions for one logical operation, even when callbacks arrive from different threads or event-loop queues. The following ordering rules are recommended:
- Validate and correlate a datagram before changing operation state.
- Mark a rate operation terminal before invoking user/application completion.
- Re-arm the host timer from the request’s current absolute deadline after every nonterminal datagram or timeout transition. A valid non-oldest response can move that deadline earlier to the preference deadline.
- Disarm or invalidate the deadline before terminal completion; a later timer callback should be harmless.
- Invalidate operation ownership before cancellation or host-request teardown; a late UDP response should be ignored.
- Schedule source-port replacement from steering feedback, but do not perform replacement reentrantly inside response parsing or a receive callback.
- A latency report MAY be queued after rate completion, but its state and failure should remain independent of the rate operation.
A duplicate valid response for an already-terminal operation should be ignored. An implementation should not invoke the application completion callback twice, send a second latency report for one admitted operation, or turn a late valid response into a new operation.
6.4. Error taxonomy
The protocol state machine distinguishes these classes:
| Error class | Examples | State-machine consequence |
|---|---|---|
Validation |
malformed local input, impossible size, unsupported credential |
Created -> Terminal; no datagram |
Discovery |
missing SRV, unusable target address, DNS timeout |
transport Unavailable; refresh according to persistent DNS configuration |
Transport |
send failure, receive error, endpoint loss | Current transmission fails locally or remains pending according to the host integration. |
Timeout |
preference, replay, or final deadline elapsed |
Select a candidate, start the next replay round, enter final receive, or finish Terminal(Failed) according to Section 9.3. |
Authentication |
failed credential validation or decryption | ignore datagram; the current transmission or final-receive state remains pending |
Correlation |
wrong tenant, operation ID, PDU kind, or server trust | ignore datagram; the current transmission or final-receive state remains pending |
Semantic |
valid response with denied guard/resource condition |
rate operation Terminal(Denied) |
Cancellation |
caller abort, host teardown, client close |
rate operation Cancelled; suppress late completion |
The API may expose a richer language-specific error type, while preserving the distinctions needed to apply the transitions and terminal-outcome rules above.
7. Client Operations
7.1. Rate Limiting (checkRateLimit)
This operation checks if a request is allowed based on defined guards and resource limits.
7.1.1. Request (PDU_RATE_REQUEST - 0x5452)
-
The client constructs a
rate_requestPDU containing:-
A list of
GuardBlocks (preconditions). -
A list of
ResourceBlocks (token requests). -
An optional
metrics_labelTLV.
-
A list of
-
If tenant metrics label cardinality is exhausted server-side, the label may be internally rewritten to the fixed overflow label
overflow. -
The PDU is wrapped in
TenantandAuthheaders. - The client sends the UDP datagram to the server(s) and waits for a response.
7.1.2. Response (PDU_RATE_RESPONSE - 0x5252)
-
The server sends back a
rate_responsePDU. - The client parses the response to determine the outcome.
-
Allowed: In state-machine terms, the request reaches
Allowedif all of the following are true:-
For every
GuardBlock,current_latency < latency_threshold. -
For every
ResourceBlock,tokens_requested(reused asdeficitin response) is0.
-
For every
-
Denied: If any guard fails or any resource has a non-zero deficit, the entire request reaches
Deniedand is considered rejected. -
Quota Enforcement: Tenant quota breaches are handled atomically as request failures, except metrics label cardinality overflow, which uses label rewrite to
overflowinstead of hard-failing the request.max_samplesis not a tenant-quota-enforced field.
7.2. Latency Reporting (reportLatency)
This is a “fire-and-forget” operation to send client-observed latency metrics to the server.
7.2.1. Request (PDU_LATENCY_REPORT - 0x524C)
-
The client constructs a
latency_reportPDU containing a list ofServiceLatencyBlocks. -
The PDU is wrapped in
TenantandAuthheaders. - The client sends the UDP datagram to the server(s).
- No response is expected for this operation.
8. Error Handling
An r-client implementation should be robust against network issues and protocol errors.
-
Timeouts: The client should expose the next absolute policy deadline to the host. A deadline event selects an available candidate, starts the next configured replay round, enters the final receive-only interval, or ends in
Terminal(Failed)as specified in Section 9.3. - Protocol Errors: The client should be able to gracefully handle malformed responses from the server.
- Authentication Errors: If authentication fails, the server drops the request and sends no response (blackhole behavior). This manifests as a timeout on the client side.
-
Quota-related Rejections: When tenant quotas are exceeded for buckets/services/latency buffer size, responses should be treated as normal request rejections under the existing success/failure rules.
max_samplesremains application-managed and is not quota-rejected.
9. High Availability
The r-client is designed to work in a high-availability environment with multiple r-server instances.
-
Request Broadcasting: The client should send the same request (with the same
unique_id) to all discovered server instances. - Independent Servers: The r-servers do not coordinate and do not share a commit authority. Deduplication is server-local; convergence comes from clients delivering the same logical operation to every server.
- Membership Snapshot: A resource request should use one immutable snapshot of discovered server identities. Asynchronously discovered membership changes apply to later requests.
-
Trusted Identity: The client MUST compare the full 64-bit
server_idfor server identity, trust decisions, response tracking, and mismatch detection. -
Age Ordering: The client decodes
server_start_sfromserver_id, prefers lower startup times, and uses the lower fullserver_idas the deterministic tie-break. - Response Acceptance: The sole MVP policy retains the valid response from the oldest responding trusted server and applies the preference deadlines in Section 9.3. It does not use quorum, reliability scoring, or server-stability filtering.
-
Deduplication: The
unique_idin theTenant Headerparticipates in the server-local deduplication key defined by the canonical wire-protocol rules.
9.1. server_id Decoding
The response Tenant Header.key_id field contains the 64-bit server_id defined by the wire protocol. Clients SHOULD decode it as follows:
-
start_s_since_2025 = server_id >> 23 -
listener_id = (server_id >> 15) & 0xFF -
node_id = server_id & 0x7FFF -
server_start_s = 1735689600 + start_s_since_2025
Where 1735689600 is January 1, 2025 00:00:00 +0000 UTC in Unix seconds.
9.2. Recommended Ranking Rule
When multiple trusted responses are available, the recommended ranking rule is:
-
Prefer lower
server_start_svalues (older servers). -
If startup times are equal, prefer the lower full 64-bit
server_id.
For clarity, server_start_s comes from the wire-level server_id; the SRV record port remains authoritative for routing.
Clients MUST still use the full 64-bit server_id as the server identity key even when they decode and compare server_start_s.
9.3. Parameterized HA Resource-Request Strategy
This section defines the recommended cross-language policy for resource requests and the sole request policy implemented by the MVP C client. It is not part of the wire protocol and does not constrain independently implemented clients that choose another policy. Latency reports remain independent, fire-and-forget operations and do not participate in this state machine.
The C API represents the strategy directly as one flat r_request_policy_t.
It has no policy discriminator, nested strategy, compatibility alias, or
alternative wait/quorum/retry execution path. Callers initialize it with
r_client_default_request_policy() and then override individual fields.
The policy separates two concerns:
- Decision: choose the valid response that governs the caller’s action.
- Delivery: make a best-effort attempt to deliver the same logical operation to every server in the request’s membership snapshot.
The separation matters because choosing a response does not prove that every other server received the request.
9.3.1. Parameters
The timing and replay shape are described by these parameters:
| Parameter | C API field | Meaning |
|---|---|---|
U |
unit_ms |
Base time unit. It may be configured directly or derived from network observations, but its value is frozen for one request. |
N |
replay_count |
Maximum number of replays after the initial transmission. There are therefore N + 1 transmission rounds. |
B(k) |
replay_gap |
Duration of transmission round k, measured in units of U, for 0 <= k <= N. This is also the gap before the next replay when no valid response has arrived. |
P(k) |
preference |
Oldest-server preference interval in round k, measured in units of U, where 0 <= P(k) <= B(k). |
F |
final_receive_units |
Duration of the final receive-only interval, measured in units of U. F = 0 disables that interval. |
P_final |
final_preference_units |
Oldest-server preference interval within the final receive-only interval, where 0 <= P_final <= F. |
C |
completion_delivery |
Completion-delivery flag: false disables the final fire-and-forget convergence send and true enables it. |
B(k) permits a fixed, linear, or exponential replay schedule. Examples
include:
-
fixed:
B(k) = b; -
linear:
B(k) = min(b + k * step, b_max); and -
exponential:
B(k) = min(b * factor^k, b_max).
The C API represents both B(k) and P(k) with r_ha_schedule_t: kind
selects R_HA_SCHEDULE_FIXED, R_HA_SCHEDULE_LINEAR, or
R_HA_SCHEDULE_EXPONENTIAL; initial_units and max_units bound the
schedule; and the growth union supplies either linear_step_units or
exponential_factor. Replay gaps must be positive. Preference values may be
zero. The C client bounds replay_count by
R_CLIENT_HA_MAX_REPLAY_COUNT, although the API-key deduplication limit
normally imposes a much smaller practical maximum.
P(k) is intentionally independent of B(k). An exponential replay backoff
can therefore reduce network traffic without forcing the client to retain a
valid response for the entire, possibly long, replay interval. Once P(k) has
elapsed, the next valid response may complete the request immediately.
The complete operation horizon is:
H = U * (sum(B(k), k = 0..N) + F)
The request deduplication TTL for this strategy is derived from H. Before the
first send, the client MUST check that the derived TTL is representable on the
wire and does not exceed the limit encoded by the API key. If it does, the
client MUST reject the configuration or choose a shorter valid schedule; it
MUST NOT silently send with a different replay contract. No replay or
completion-delivery send may be initiated at or after the deduplication
deadline.
9.3.2. Per-request state
For each resource request, the client records:
- the immutable membership snapshot and deterministic oldest-to-youngest ranking over its server identities;
-
the encoded logical request and its single
unique_id; - the current round and its preference and replay deadlines;
- the set of servers from which a valid response has arrived; and
- the current candidate, which is the response from the oldest server in that set.
Server age is ordered first by server_start_s. The MVP C client resolves ties
by preferring the lower full 64-bit server_id, so one request has an
unambiguous order.
Every transmission and replay for the operation MUST represent the same
logical request and reuse the same unique_id. A client may regenerate
transport framing when required, but it MUST preserve the wire-level
deduplication identity.
9.3.3. Event algorithm
At the start of round 0, the client sends the request to every server in the
membership snapshot. At the start of a later transmission round, it replays
the request only to servers from which no valid response has yet arrived.
For each event in a transmission round:
-
Validate and correlate received datagrams before changing request state.
A response is usable only when its authentication, framing, tenant,
unique_id, response kind, and trusted server identity are valid. - Ignore invalid, unrelated, or untrusted datagrams. A duplicate valid response does not add another responding server or replace the candidate, but the current preference deadline still governs whether the existing candidate is now selectable.
- For a valid response, mark its server as having responded and replace the candidate if that server is older than the current candidate’s server.
- If the response is from the oldest server in the membership snapshot, select it immediately.
- If the round’s preference deadline has already elapsed, select the current candidate immediately.
- At the preference deadline, select the candidate if one exists. Otherwise, continue waiting until the replay deadline.
- At the replay deadline, if no candidate exists and the replay budget remains, start the next round and replay to the currently missing servers.
-
After the last transmission round expires without a valid response, enter
the final receive-only interval when
F > 0; otherwise report transport failure.
The final receive-only interval applies the same response-ranking rule, but sends no replay. At its preference deadline, the client selects its current candidate if one exists. Any valid response arriving after that deadline is selected immediately. If its end deadline arrives first, the client reports transport failure.
When a receive event and a deadline are simultaneously ready, an event-loop implementation SHOULD process a bounded batch of already-readable datagrams before applying the deadline. This avoids making the result depend only on whether the operating system reported the timer or socket first, while still preventing receive starvation.
Allow and deny are equally valid responses and use exactly the same ordering and completion rules. Once a response is selected, later responses MUST NOT change the completed result.
9.3.4. Completion delivery
When C = 1, immediately before returning any selected response, the client
performs one fire-and-forget send of the same logical request to every server
in the membership snapshot from which no valid response has arrived. It does
not wait for these sends, and their success or failure does not change the
selected response.
This completion send is outcome-independent: it is performed for both allowed
and denied responses. If one server denies while a missing, out-of-sync server
would allow and consume the request, delivering the operation to that server
is intentional. It makes the servers’ operation histories more alike and
allows their TTL-governed state to converge; it is not a second client
decision. Reusing the same unique_id also means that a server which processed
an earlier copy but lost its response deduplicates the completion send.
The absence of a valid response is only evidence that delivery may have failed; it is not proof. Completion delivery is therefore a best-effort convergence mechanism, not an immediate consistency guarantee.
9.3.5. Default schedule
The recommended initial configuration preserves the bounded three-phase heuristic:
U = 20 ms
N = 1
B(0) = 1
B(1) = 1
P(0) = 1
P(1) = 1
F = 1
P_final = 0
C = 1
TTL = 3 * U
With this schedule, the client:
-
sends to all servers and waits for the oldest response for at most
U, retaining the oldest valid fallback; -
if no server responded, replays to all still-missing servers and applies
the same rule for another
U; and -
if no server has responded after both transmissions, waits for one final
Uand returns the first valid response immediately.
At any phase, a response from the oldest server completes the request
immediately. At either of the first two deadlines, the oldest valid response
received so far completes it. With C = 1, every selected allow or deny is
preceded by the fire-and-forget completion delivery to servers still missing a
valid response.
9.4. DNS Refresh Is Separate
DNS refresh pacing belongs to persistent client configuration, not to the
resource-request response-selection policy. In the C API it is configured
through r_client_config_t.dns_refresh:
-
refresh_interval_mscontrols periodic refresh and defaults to 300 seconds when zero; -
forced_refresh_min_interval_msbounds repeated forced refresh and defaults to one second when zero; and -
forced_refresh_jitter_msadds optional jitter and defaults to zero.
The C client copies r_request_policy_t during r_client_create(). A request
then freezes its membership snapshot and derived schedule; DNS results
installed asynchronously apply to later requests.