rl-c-client

Ratelimitly C Client

What Ratelimitly does

Ratelimitly, a distributed admission-control service, decides whether an application may begin work that consumes configured resources. The decision may also depend on whether recently observed service latencies remain below application-defined thresholds.

rl-c-client is the public C11 library through which an application requests those decisions and, independently, contributes latency measurements used by future decisions.

Core operations

The library exposes two independent operations:

An application may use either operation without the other. A common workflow is to request permission for some work, perform it only after a grant, and then optionally report measured latencies for services used by that work. This is only one application workflow: a reporter may send only latency reports, and a resource consumer may send only resource requests. Reports influence resource requests only through the latency trackers; the two operations are not paired.

flowchart LR
    Consumer["Resource-consuming application"]:::neutral --> Request["Resource request<br/>intended consumptions + optional guards"]:::neutral
    Request --> Evaluate["Ratelimitly<br/>atomic admission decision"]:::neutral
    Evaluate --> Decision{"Granted?"}:::neutral
    Decision -->|No| Rejected["No resources consumed"]:::danger
    Decision -->|Yes| Granted["Resources consumed<br/>application may perform work"]:::success

    Reporter["Same or another application"]:::neutral --> Report["Optional latency report<br/>measured service latencies"]:::neutral
    Report --> Trackers["Latency trackers"]:::neutral
    Trackers -. "input to latency guards" .-> Evaluate

    classDef neutral fill:#EAECEF,stroke:#7D8590,color:#1A1A1A;
    classDef danger fill:#FCE8E6,stroke:#B0413E,color:#1A1A1A;
    classDef success fill:#E6F4EA,stroke:#1E7E45,color:#1A1A1A;

Three small examples

Each example first states the operation in English and then expresses it with the public C API. The snippets assume that r_client.h is included, client is initialized, and a non-null completion callback is supplied for resource requests. Request lifetime and event-loop handling are covered later.

Request one token

In English: “Get me one token for checkout, whose limit is 100 tokens per second.”

int request_one_checkout_token(
    r_client_t *client,
    r_rate_limit_cb callback,
    void *user,
    r_client_req_t **out_request
) {
    r_resource_request_t resource = {
        .window_size_ms = 1000u,
        .rate_limit = 100u,
        .tokens_requested = 1u,
    };
    int rc = r_client_derive_bucket_id(
        "checkout",                  /* exact bucket-name bytes */
        sizeof("checkout") - 1u,     /* bucket-name byte length */
        resource.window_size_ms,     /* configured window */
        resource.rate_limit,         /* configured rate */
        resource.bucket_id           /* resulting 16-byte bucket ID */
    );
    if (rc != RCLIENT_OK) {
        return rc;
    }

    return r_client_check_rate_limit_async(
        client,       /* initialized client */
        &resource,    /* resource consumptions */
        1u,           /* number of resource consumptions */
        NULL,         /* no latency guards */
        0u,           /* number of latency guards */
        NULL,         /* no metrics label */
        0u,           /* metrics-label length */
        callback,     /* receives grant, rejection, or failure */
        user,         /* application callback context */
        out_request   /* request handle for timers or cancellation */
    );
}

A grant consumes one token from checkout and authorizes the operation. A rejection consumes nothing.

A request failure is neither a grant nor a rejection. It means the client did not obtain a usable decision—for example, because of a timeout, delivery problem, or invalid response. The application must handle this outcome separately according to its failure policy. Because a failure may occur after the request was sent, it does not prove that Ratelimitly did not process the request. This third outcome applies to guarded resource requests as well.

Report one service latency

In English: “Record that one call to inventory took 18 ms.”

int report_inventory_latency(r_client_t *client) {
    r_service_latency_report_t report = {
        .observed_latency = 18u,
        .ttl_ms = 10000u,
        .max_samples = 100u,
        .buffer_size = 32u,
        .min_sample_threshold = 5u,
    };
    int rc = r_client_derive_latency_tracker_id(
        "inventory",                    /* exact tracker-name bytes */
        sizeof("inventory") - 1u,       /* tracker-name byte length */
        report.ttl_ms,                   /* sample lifetime */
        report.max_samples,              /* samples considered */
        report.buffer_size,              /* tracker storage */
        report.min_sample_threshold,     /* warm-up sample count */
        report.latency_tracker_id        /* resulting 16-byte tracker ID */
    );
    if (rc != RCLIENT_OK) {
        return rc;
    }

    return r_client_report_latency(
        client,   /* initialized client */
        &report,  /* latency reports */
        1u        /* number of latency reports */
    );
}

The report contributes that measurement to the inventory latency tracker. It does not consume a resource or make an admission decision. The other values configure that tracker and are explained in Latency Guards and Independent Reports.

Request one token with one latency guard

In English: “Get me one token for checkout, but only if the tracked inventory latency is below 100 ms.”

int request_checkout_with_inventory_guard(
    r_client_t *client,
    r_rate_limit_cb callback,
    void *user,
    r_client_req_t **out_request
) {
    r_resource_request_t resource = {
        .window_size_ms = 1000u,
        .rate_limit = 100u,
        .tokens_requested = 1u,
    };
    int rc = r_client_derive_bucket_id(
        "checkout",                  /* exact bucket-name bytes */
        sizeof("checkout") - 1u,     /* bucket-name byte length */
        resource.window_size_ms,     /* configured window */
        resource.rate_limit,         /* configured rate */
        resource.bucket_id           /* resulting 16-byte bucket ID */
    );
    if (rc != RCLIENT_OK) {
        return rc;
    }

    r_latency_guard_t guard = {
        .threshold_ms = 100u,
        .ttl_ms = 10000u,
        .max_samples = 100u,
        .buffer_size = 32u,
        .min_sample_threshold = 5u,
    };
    rc = r_client_derive_latency_tracker_id(
        "inventory",                    /* exact tracker-name bytes */
        sizeof("inventory") - 1u,       /* tracker-name byte length */
        guard.ttl_ms,                    /* sample lifetime */
        guard.max_samples,               /* samples considered */
        guard.buffer_size,               /* tracker storage */
        guard.min_sample_threshold,      /* warm-up sample count */
        guard.latency_tracker_id         /* resulting 16-byte tracker ID */
    );
    if (rc != RCLIENT_OK) {
        return rc;
    }

    return r_client_check_rate_limit_async(
        client,       /* initialized client */
        &resource,    /* resource consumptions */
        1u,           /* number of resource consumptions */
        &guard,       /* latency guards */
        1u,           /* number of latency guards */
        NULL,         /* no metrics label */
        0u,           /* metrics-label length */
        callback,     /* receives grant, rejection, or failure */
        user,         /* application callback context */
        out_request   /* request handle for timers or cancellation */
    );
}

Ratelimitly evaluates the consumption and guard together. A grant consumes one token and authorizes the operation; if either condition fails, the complete request is rejected and nothing is consumed. A request failure instead means that no usable combined decision was obtained, not that either condition rejected the request.

These are logical operations, independent of how the client delivers them. Server discovery, request delivery, response selection, replays, and report-delivery behavior belong to the API and policy layers documented in docs/api.md.

Integrate the library

Applications differ in whether they already own UDP sockets, DNS, timers, and logging or prefer a ready-made runtime. The C client supports core, optional workflow, and public-runtime integration levels without changing the two logical operations above.

Choose the ownership boundary and request-buffer lifetime model in Choosing an integration layer.

Install a release

Published assets are available from GitHub Releases. Choose the asset built for the target system and architecture:

Target Architectures Payload
Ubuntu 24.04 (ubuntu24.04) amd64, aarch64 Runtime and development .deb packages
Debian 13 (debian13) amd64, aarch64 Runtime and development .deb packages
Fedora 44 (fedora44) amd64, aarch64 Runtime and development .rpm packages
macOS amd64, aarch64, universal2 Relocatable SDK .tar.gz
Windows amd64, aarch64 Relocatable SDK .zip
Source Platform-independent Embeddable .tar.gz and .zip

Linux applications normally install both packages. Replace <VERSION> and <ARCH> with the release version and amd64 or aarch64:

# Ubuntu 24.04
sudo apt install \
  ./rl-c-client-v<VERSION>-ubuntu24.04-<ARCH>-runtime.deb \
  ./rl-c-client-v<VERSION>-ubuntu24.04-<ARCH>-development.deb

# Debian 13
sudo apt install \
  ./rl-c-client-v<VERSION>-debian13-<ARCH>-runtime.deb \
  ./rl-c-client-v<VERSION>-debian13-<ARCH>-development.deb

# Fedora 44
sudo dnf install \
  ./rl-c-client-v<VERSION>-fedora44-<ARCH>-runtime.rpm \
  ./rl-c-client-v<VERSION>-fedora44-<ARCH>-development.rpm

The Linux runtime packages use the distribution’s OpenSSL libcrypto. The development packages install public headers, static and shared link artifacts, pkg-config metadata, and a CMake package:

find_package(rclient CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE rclient::rclient)

The macOS SDKs use the same CMake and pkg-config interfaces. The macos-universal2 SDK contains both Intel and Apple Silicon slices; the architecture-specific SDKs are smaller. Release binaries target macOS 12.0 or newer. OpenSSL is built from pinned source at that same deployment target instead of using runner-local binaries. The shared dylib contains its OpenSSL code and has an @rpath install name. Consumers of the static archive must request the static CMake component and provide static OpenSSL at final link time:

find_package(rclient CONFIG REQUIRED COMPONENTS static)
target_link_libraries(my_app PRIVATE rclient::static)

Shared-library consumers do not need OpenSSL development files. Each bundled SDK includes the exact OpenSSL license under share/doc/rl-c-client/third-party and a dependency inventory at share/rl-c-client/dependencies.spdx.json.

The windows-amd64 and windows-aarch64 SDKs contain rclient.dll, import and static libraries, headers, CMake metadata, SPDX SBOMs, third-party licenses, and toolchain metadata. The DLL is built with pinned WDK/MSVC tools, the static MultiThreaded (/MT) runtime, and static OpenSSL. Its import table is checked to exclude the separately installed Visual C++ runtime and OpenSSL DLL families. A build using the SDK’s shared CMake target needs no external OpenSSL development tree; the optional static component still requires one.

Every release also contains RELEASE-MANIFEST.json and SHA256SUMS. After downloading the complete asset set, verify the hashes:

sha256sum --check SHA256SUMS

To verify one downloaded asset without downloading all payloads:

grep '  rl-c-client-v<VERSION>-source.tar.gz$' SHA256SUMS |
  sha256sum --check -

GitHub also records build provenance for every published asset:

gh attestation verify rl-c-client-v<VERSION>-source.tar.gz \
  --repo ratelimitly-com/rl-c-client

Use rl-c-client-v<VERSION>-source.tar.gz or the equivalent source.zip when the client should be compiled directly into an application. See EMBEDDING.md for the two supported source-integration modes.

Build

Requirements:

Build the static and shared libraries:

make

Outputs:

Run an optional combined-workflow example

The core operations may be used independently. The latency-tracker example instead demonstrates the optional convenience workflow: it submits one resource consumption and one latency guard, runs protected work only after the combined request is granted, and reports the work’s measured service latency:

make
make -C examples/latency_tracker
RATELIMITLY_AUTH_KEY=rl-aes1... \
  ./examples/latency_tracker/latency-tracker-example

For a credential-free deterministic run against the repository’s synthetic responder, build its prerequisites first, then execute the script:

make test-responder
make tests/test_latency_tracker
bash tests/test_latency_tracker.sh

(Running make test also builds both.)

Build the perf client:

make perf_client

Run local tests:

make test

Build the optional deterministic protocol fixture used by downstream integration tests:

make test-responder

Its test-support contract and deterministic scenarios are documented in docs/test-responder.md. The executable is not part of the production library API and contains only synthetic credentials.

Clean generated files:

make clean

On macOS with Homebrew OpenSSL, the Makefile defaults OPENSSL_PREFIX to /opt/homebrew/opt/openssl@3. Override it when needed:

OPENSSL_PREFIX=/custom/openssl make

Public API

The public headers are:

Do not include files from src/; they are private implementation details.

Core client (r_client.h):

Optional admission workflow (r_client_workflow.h):

Optional public runtime (r_client_runtime.h):

See docs/api.md for the API contract and IO_ABSTRACTION.md for event-loop integration.

Integration Examples

examples/README.md contains buildable, commented integrations using only public headers. Start with the example matching the host application’s ownership model:

Each source begins with numbered control flow and explicit ownership rules. The integration guide adds dependency-specific build commands, run commands, shutdown behavior, limitations, and production notes. Repository tests verify the inventory, public-header boundary, and standalone latency workflow.

API Key Credentials

Ratelimitly API key credentials are Bech32 strings:

These are the only credential forms this client accepts; the protocol’s unauthenticated mode is deliberately not implemented, and any other credential is rejected with RCLIENT_ERR_CONFIG.

Use rl-aes... credentials for deployments that cross an untrusted network: AES mode encrypts the request payload and authenticates the entire datagram. Packet headers (tenant key ID, request ID, timestamp) stay plaintext by design, so an on-path observer can see who is talking, but cannot read or alter request contents. Cookie mode is a private-network mode: the cookie is sent on the wire and does not authenticate the packet contents, so it must be used only where on-path modification and capture are outside the deployment threat model.

Command lines and process listings leak credentials: prefer passing keys through the environment (as the runtime’s RATELIMITLY_AUTH_KEY does) over --auth=-style arguments outside development. See SECURITY.md for full credential-handling guidance.

The encoded key is the source of truth for the tenant key ID, authentication type, and quota values. The default production tenant DNS name is c-<key-id>.p0.ratelimitly.com, so normal configuration needs only the key:

r_client_config_t cfg = {0};
cfg.tenant.auth.secret = auth_key;

Set cfg.tenant.dns_name only to override production discovery for a custom, development, or staging DNS zone. An override zone must publish SRV target hostnames whose first label encodes each server’s ID as s-<decimal> (see IO_ABSTRACTION.md, DNS) — targets without that label are silently ignored, and submissions return RCLIENT_ERR_DNS if none remain. Nonzero cfg.tenant.key_id and cfg.tenant.auth.type values are optional assertions; when supplied, they must match the encoded key. r_client_parse_auth_key remains available for callers that want to inspect key metadata before creating a client.

cfg.tenant.auth.secret is the encoded Bech32 credential string, not raw secret bytes. Leave cfg.tenant.auth.secret_len as 0 for a normal null-terminated credential string, or set it to the encoded string length if the credential is not null-terminated. The client decodes the raw 32-byte cookie/AES material internally after validating the Bech32 credential.

Do not log the secret field of the r_auth_key_info_t returned by r_client_parse_auth_key; it contains raw credential material for cookie and AES keys.

Core event-loop model

The core client never waits on a socket and does not create threads. Host I/O and resolver callbacks may complete synchronously, and the optional public runtime performs synchronous DNS during initialization or refresh. A custom core integration must:

  1. Provide r_io_ops_t with UDP send, current time, and optional steering feedback (the log hook is reserved and currently never invoked).
  2. Provide r_resolver_ops_t for SRV and A/AAAA lookup.
  3. Call r_client_check_rate_limit_async or the borrowed variant.
  4. Schedule the deadline from r_client_request_deadline_ms.
  5. Deliver UDP responses through r_client_on_datagram.
  6. Call r_client_on_timeout when request timers fire.

Response replay protection is scoped to this request lifecycle: AES responses must carry a matching authenticated unique_id for an in-flight request, and datagrams for completed, timed-out, or canceled requests are ignored.

For integrations with request-scoped memory pools, use r_client_check_rate_limit_async_borrowed when request buffers live until callback completion.

Content-defined IDs

Resource buckets and latency trackers are identified by their names together with the settings that define their stored state. Use r_client_derive_bucket_id() for a bucket and r_client_derive_latency_tracker_id() for a latency tracker. The workflow API does this automatically from bucket_name, latency_tracker_name, and their configuration.

The threshold is not part of a latency-tracker ID: it is a condition evaluated against the tracker, not part of the tracker’s stored state. The observed latency is likewise a sample, not tracker identity. See docs/api.md for the exact contracts and examples.

Perf Client

The perf client is a standalone load generator and smoke-test tool.

Examples:

bin/perf_client --clients=50 --requests=10000 --auth=rl-aes1...
bin/perf_client --duration=60 --auth=rl-aes1...
bin/perf_client --srv=api-key.example.com --duration=30 --clients=50 --auth=rl-aes1...
RCLIENT_DNS_SERVER=127.0.0.1:5353 bin/perf_client --auth=rl-aes1...
bin/perf_client --unit-ms=20 --replay-count=1 --auth=rl-aes1...

Without --srv, the perf client derives c-<key-id>.p0.ratelimitly.com from --auth, matching the library default. Use --srv only for a custom, development, or staging DNS zone. That zone must publish conforming s-<decimal> SRV targets; otherwise startup has no usable membership and requests fail with RCLIENT_ERR_DNS.

HA-policy flags:

These map to the strategy’s base unit U and replay count N. The perf client uses the default fixed replay and preference schedules, final receive-only interval, and completion delivery. Applications can configure the full strategy through r_request_policy_t.

Glossary

Term Meaning
Ratelimitly Distributed admission-control service that atomically evaluates resource consumption and optional latency guards.
r-server Ratelimitly server discovered by the client and sent resource requests or latency reports.
resource request Request containing zero or more resource consumptions and zero or more latency guards. Non-empty requests are evaluated atomically by r-servers; the empty request succeeds locally without network activity.
resource consumption Quantity requested from one configured resource bucket as part of a resource request.
latency report Independent operation that contributes one or more measured service latencies to latency trackers.
service Application-defined name for a measured operation or dependency; together with its tracker settings, it defines a canonical latency-tracker ID.
C11 2011 revision of the C language standard required by this library.
README Repository overview document that introduces a project and links to its detailed guides.
GLib Portable core library whose main loop is used by one integration example.
GIO GLib input/output APIs, including the GIOChannel socket wrapper used by that example.
CivetWeb Embedded C HTTP server represented by one self-contained integration.
GNU Free-software project under which libmicrohttpd is developed.
H2O Event-driven C HTTP server represented by one self-contained integration.
LICENSE Repository file containing the terms under which the source may be used and redistributed.
MIT Permissive open-source license applied to this repository.
admission Application-level use of a resource-request decision to decide whether work should begin.
resource rate limit Configured capacity for a resource bucket over a time window.
bucket Stable resource identity whose configured quota is consumed by matching requests.
token Unit of consumption against a bucket; a request asks for a token count and a grant consumes exactly that count.
token deficit Per-resource result value: 0 means granted; nonzero is how many requested tokens the bucket could not supply (the whole request is then rejected and nothing is consumed).
actual rate Per-resource result value: the bucket’s currently consumed token count in its window, as reported by the responding server.
server ID 64-bit server identity carried in responses and encoded in SRV target labels as s-<decimal>; its upper bits encode the server start time used for oldest-server preference.
deduplication TTL Server-side at-most-once window requested by each resource request; duplicates of the same request within it replay the cached response instead of being re-processed.
metrics label Optional request tag for per-label server-side metrics, bounded by the key’s label-cardinality quota; overflowing labels are rewritten to overflow.
SBOM Software bill of materials — the dependency inventory (SPDX format) shipped with release artifacts.
latency guard A request to shed new work when the tracker’s recent service latency reaches its configured threshold.
latency tracker Server-side sample window identified by a canonical tracker ID and defined by its lifetime, sample count, buffer size, and warm-up threshold.
tenant Isolated Ratelimitly account identified by metadata encoded in the API key.
host loop The application’s existing event loop; it owns readiness callbacks and timers around the client.
public runtime Optional adapter that owns nonblocking UDP sockets and production DNS discovery while exposing readiness and deadlines to the host loop.
SRV DNS service record that locates a service by returning target hostnames and ports.
AAAA DNS address record that maps a hostname to an IPv6 address.
IPv4 Internet Protocol version 4, the widely deployed 32-bit network address format.
IPv6 Internet Protocol version 6, the modern network address format represented by an AAAA record.
Bech32 credential Checksummed, human-readable key encoding used for rl-cookie... and rl-aes... credentials.
AES Advanced Encryption Standard, the symmetric cipher used by rl-aes... credentials.
GCM Galois/Counter Mode, which adds authentication to AES encryption.
POSIX Portable Operating System Interface, the Unix-like APIs used by Linux and macOS builds.
backpressure Pausing new input or work until a downstream operation has capacity again.
steering feedback Server hint that lets a host rebind a UDP source port for later requests.

References

Repository Status

This repository is licensed under the MIT License; see LICENSE.