Skip to main content

Sonde

PyPI CI OpenSSF Scorecard License

Probe any HTTP API for its rate limits, burst ceiling, and full-scrape time. Provider-pluggable, safe by default.

Install

Requires Python 3.12+.

pip install sonde

From source:

git clone https://github.com/Jartan-LLC/sonde.git
cd sonde
pip install -e .

For Docker, see Docker below.

Quick Start

Probe the Roblox asset-owners endpoint:

export ROBLOX_COOKIE="your_roblosecurity_cookie"
sonde asset-owners --asset-id 20573078 --total-items 1470000

Probe GitHub stargazers:

export GITHUB_TOKEN="ghp_..."
sonde github-stargazers --owner Jartan-LLC --repo sonde

Without GITHUB_TOKEN, sonde probes anonymously, at GitHub's lower rate limit.

Results are written to sonde_report.json by default:

sonde asset-owners --asset-id 20573078 --output my_report.json

How It Works

Sonde runs five phases against the target endpoint, then combines the measurements into a safe rate estimate.

Phase What it does
Sanity One request. Validates auth, reads rate-limit response headers (e.g. x-ratelimit-limit, x-ratelimit-remaining), and records items-per-page for the scrape-time estimate.
Sequential Fires back-to-back requests (up to --seq-cap, default 150) until the first 429 or the cap. Measures baseline throughput and how many requests the API allows before throttling.
Burst Fires N truly-concurrent requests (default sizes: 10, 20, 40, 80) via httpx on a single asyncio event loop. After the first throttled burst, measures the recovery window -- how long until requests succeed again -- via adaptive geometric backoff.
Sweep Drains the rate-limit bucket, then paces requests at progressively faster intervals (default: 8s down to 0.15s) to find the fastest sustainable interval from empty. Skipped by default when authoritative rate-limit headers are present (override with --force-sweep).
Estimate Combines all measurements into a recommended request interval and, if a total item count is known, a wall-clock full-scrape estimate.

How the estimate is produced

The estimate phase uses a priority ladder to determine the safe rate:

  1. Authoritative headers -- If the API returned x-ratelimit-limit and a window, use those directly (e.g. 100 requests per 60s).
  2. Swept floor -- If the sweep found a fastest sustainable interval, use that.
  3. Token-bucket inference -- If burst results show a clean burst size and a measured recovery window, infer the bucket rate.
  4. Sequential fallback -- Use the observed sequential throughput before the first 429.
  5. No-throttle fallback -- If nothing ever throttled, no ceiling was found, so fall back to a conservative fraction of the measured sequential throughput.

Every rung applies the safety margin (default 80%, configurable with --margin) -- the recommended pace is ~25% slower than the measured ceiling. Rung 5 has no measured ceiling, so it applies an extra 0.5 factor on top (~40% of observed throughput at the default margin).

Endpoints

asset-owners

Roblox inventory.roblox.com/v2/assets/{id}/owners -- paginated list of owners of a collectible asset.

Option Required Default Description
--asset-id Yes -- Asset ID to probe (e.g. 20573078)
--sort-order No Asc Asc or Desc
--page-size No 100 Items per page (capped at 100)
--total-items No None Known total owners, for wall-clock estimate

Auth: Set ROBLOX_COOKIE (legacy web-session) and/or ROBLOX_BEARER (Open Cloud) environment variables.

github-stargazers

GitHub api.github.com/repos/{owner}/{repo}/stargazers -- users who starred a repository.

Option Required Default Description
--owner Yes -- Repository owner or organization (e.g. Jartan-LLC)
--repo Yes -- Repository name (e.g. sonde)
--page-size No 100 Items per page (capped at 100)
--total-items No None Known stargazer count, for wall-clock estimate

Auth: Set GITHUB_TOKEN environment variable. Without it, you get the anonymous rate limit (60 requests/hour).

Adding an Endpoint

Write the endpoint in your own package. Once the package is installed alongside sonde, the endpoint is a sonde subcommand.

  1. Subclass Endpoint, set a unique name (the subcommand) and a one-line help, and implement build_request(cursor) and parse_page(response).

  2. Override make_provider() to return the API's Provider: the generic one handles standard 200/429 and IETF headers, and GitHubProvider and RobloxProvider are built in. A provider that sends credentials in headers lists them in credentials(), so logs redact them; query-parameter credentials go in its auth_params(), not in an endpoint's request parameters.

  3. Optionally implement total_items() for scrape-time estimates, add_arguments() / from_args() for CLI options, and extra_headers() for endpoint-specific headers.

  4. If the endpoint is paginated, call add_pagination_args(parser, page_max=...) in add_arguments() and pagination_from_args(args, page_max=...) in from_args(), with the API's largest page size, so it gets the shared --page-size / --total-items flags (clamped to that size).

  5. Declare the class in your package's pyproject.toml:

    [project.entry-points."sonde.endpoints"]
    my-endpoint = "my_package.endpoints:MyEndpoint"
    

Import what you need from sonde: its exports are the plugin API. While sonde is 0.x, a minor release can change them, and the changelog says how.

Minimal example:

from typing import Any, override

from sonde import Endpoint, PageResult, RequestSpec


class MyEndpoint(Endpoint):
    name = "my-endpoint"
    help = "one-line description for --help"

    @override
    def build_request(self, cursor: Any) -> RequestSpec:
        return RequestSpec(url="https://api.example.com/items", params={"page": cursor or 1})

    @override
    def parse_page(self, response: Any) -> PageResult:
        data = response.json()
        return PageResult(count=len(data["items"]), next_cursor=data.get("next_page"))

A built-in endpoint lives in src/sonde/endpoints/ instead, decorated with @register and imported in that package's __init__.py.

CLI Reference

Common options shared by all endpoints:

Option Default Description
--max-requests 1200 Hard global cap across all phases (safety budget)
--seq-cap 150 Max sequential requests before stopping
--skip-burst off Skip the concurrent burst phase
--burst-sizes 10,20,40,80 Comma-separated list of concurrent burst sizes
--burst-cooldown 60.0 Fallback seconds between bursts if the recovery window can't be measured
--recovery-step 0.25 Initial poll delay when measuring the throttle window (grows geometrically)
--recovery-max 90.0 Give up measuring the window after this many seconds
--recovery-polls 15 Max polls during recovery measurement
--skip-sweep off Skip the sustained-interval sweep phase
--force-sweep off Run the sweep even when authoritative rate-limit headers are present
--sweep-intervals 8,5,3,2,1.2,0.6,0.3,0.15 Inter-request intervals (seconds) to test, slow to fast
--sweep-count 20 Paced requests per interval after draining
--sweep-drain 500 Cap on rapid requests used to empty the bucket before each interval
--sweep-tolerance 0.1 Max fraction of 429s for an interval to count as sustainable
--margin 0.8 Safety margin: pace at 80% of the measured max rate (0.8 = 25% slower than ceiling)
--output sonde_report.json Path for the JSON report (use - for stdout)
-v / --verbose off Show per-request detail (sets log level to DEBUG)
-q / --quiet off Only show warnings and errors (sets log level to WARNING)
--log-format plain Log line format: plain (message-only) or json (structured)

-v and -q are mutually exclusive. Logs always go to stderr; the report goes to --output.

Exit codes: 0 success, 2 precondition failure (bad arguments, an endpoint plugin that won't load, unwritable --output, or the endpoint returned no usable response), 1 unexpected crash, 130 interrupted.

Piping and machine-readable output

Use --output - to write the JSON report to stdout instead of a file. Combine with -q to suppress INFO-level log noise on stderr:

sonde asset-owners --asset-id 20573078 --output - -q | jq .estimate

Use --log-format json for structured log lines on stderr (keys: timestamp, level, logger, message, plus exc on error lines), useful for log aggregators or CI pipelines:

sonde asset-owners --asset-id 20573078 --log-format json 2>sonde.log

Docker

Build:

docker build -t sonde .

Run (mount current directory so the report lands on the host):

docker run --rm -v "$(pwd):/data" -e ROBLOX_COOKIE sonde \
    asset-owners --asset-id 20573078 --total-items 1470000
docker run --rm -v "$(pwd):/data" -e GITHUB_TOKEN sonde \
    github-stargazers --owner Jartan-LLC --repo sonde

The container writes sonde_report.json to /data (the mounted volume).

Development

Setup and the checks to run before a pull request are in CONTRIBUTING.md.

License

MIT

Metadata

Release files for sonde 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sonde 0.2.0
File Size Uploaded
sonde-0.2.0.tar.gz 55.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sonde 0.2.0
File Interpreter ABI Platform
sonde-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 95.2 kB

Release files / sonde-0.2.0.tar.gz

Download URL sonde-0.2.0.tar.gz
Size 55.8 kB
Tags Source
SHA-256 checksum
How to use checksums
d68c496148e22553d0455d251f27efd987ea1cd14cd67dfb1465dbc7aee5ffe9
BLAKE2b-256 checksum
How to use checksums
ab7cd5b94b9e11e6e46fecb389c0fe068e3a92f402f51660a3cd5dbc935d3b90
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / sonde-0.2.0-py3-none-any.whl

Download URL sonde-0.2.0-py3-none-any.whl
Size 39.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
759bb5e00cd7a1b2f736db2fe239094c420c6792f2adc926d14bd1bb36c7584c
BLAKE2b-256 checksum
How to use checksums
e1f581eb3e7cf3e1ce3565d4f4131b8c3379248f2284a33f913bf2e955bad8d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page