Skip to the guide
All guides

Limits

What Reqtide does and does not do today. State, deadlines, retries, shared state, tested platforms, what the library commits to, and how versions work.

app Reqtide PHP 0.1.0checked reading 7 minutes

On this page

The first release is 0.1.0 (beta). The API can change between minor versions, and every change is listed in the changelog. See Install.

Any public name or behavior may change before the first release. The core package enaxon/reqtide keeps its state in one process. The Redis and Guzzle packages are separate packages in the repository. They are described in Shared state with Redis and Guzzle requests with Reqtide.

Tested platforms

The tests and the examples were run in Docker on the command line with PHP 8.3, 8.4 and 8.5. PHP-FPM is covered by three CI lanes, one each for PHP 8.3, 8.4 and 8.5, with nginx, six workers and Redis 8.2.10 (process-local state starts fresh per request, Redis state is shared and correct under concurrent requests, a bulkhead, a deadline and a retry). The Guzzle client under FPM, Apache and other web server setups were not run. The Guzzle package was also run through nginx 1.28 as a reverse proxy in front of a test server, with buffered and unbuffered request bodies, a slow chunked response and a response that never ends (the attempt timeout stops it). Streaming bodies and other proxies were not run. A passing local run is no support claim and no production claim.

Reqtide makes no speed, capacity or latency claims. The only figures are the rough cost numbers in Guzzle requests with Reqtide. They were measured once on one laptop, with large noise.

What the library does not do

TopicWhat it means for you
State lives in one PHP processUnder PHP-FPM a pipeline built inside a request starts every request with a full quota and a closed circuit. Limits do not add up across requests, workers or servers. Use the core package where one process lives long enough to matter, such as a worker, a command line job or a daemon. Otherwise use the Redis package.
No shared quota in the core packageA rate limit in the core package protects one process. Ten workers each get the full limit. A shared quota needs the Redis package.
A running callback is not interruptedThe total deadline is checked between steps. If your callable runs past the deadline, it runs to the end, and Reqtide reports the late result as DeadlineExceeded. The synchronous runtime cannot enforce an attempt timeout. You declare it as Cooperative, and your code must stop itself.
A bulkhead lease is not a physical stopWhen a lease expires, the callback may still be running. The permit is free again, but the work is not stopped. Another caller can then be admitted past the bulkhead limit, and a slow call that fails late does not count toward the circuit breaker. Give your callable its own hard timeout, shorter than the attempt timeout (for example the timeout of your HTTP or database client). The Guzzle package does this for you. When a lease ran out, the result carries the diagnostic concurrency_release_stale. Before each dispatch Reqtide checks, with its own monotonic clock, that a concurrency permit and a half-open probe still outlast the attempt; if not, the attempt is not sent and ends with ownership_lease_insufficient. This check is local and conservative. It is not a fencing token, and a pause after the check cannot be seen. A system that must be protected from a paused or slow holder needs its own fencing, for example a version or token that it checks itself; Reqtide does not provide a fencing token.
Rate charges are not refundedA call cancelled after admission and before dispatch still used its unit. Quota used is not the same as requests sent.
No exactly-once executionA call may run more than once, or once with an unknown result. Reqtide reports what it knows. It cannot make your operation idempotent.
Retries need your permissionA retry needs your assertion that repeating the call is safe, a retry budget and a retryable exception class. Reqtide never infers safety, and it has no HTTP knowledge.
No defaultsYou choose every timeout, limit, window and delay. The numbers in the examples make behavior visible. They are no recommendation.
No failover guaranteesThe Redis package keeps its state in Redis memory. A restart without persistence, a flush or a failover loses it. Strict failover guarantees are out of scope. Sentinel, Cluster and Valkey are not supported.
ScopeReqtide wraps a callable. It does not send requests, store work for later or defend a public endpoint.
Not in the first releaseHedged requests, cache revalidation, a failed-work queue and waiting queues.
What Reqtide does not do and what that means for your code.

What Reqtide commits to

These points hold for the local part of the library.

  • One operation, one deadline. A retry whose delay cannot fit in the remaining time is not started.
  • Your exceptions come out unwrapped. The refusals of Reqtide are typed exceptions that carry a machine-readable reason. See Error handling.
  • Nothing is retried unless you say so. Retrying needs your safety assertion, a retry budget and a retryable exception class.
  • A permit is released at most once, or ends by lease expiry. A circuit permit or a concurrency permit is released on success, on exception and on cancellation. If a release fails, Reqtide records the failure and the permit ends when its lease expires.
  • Failing state is not hidden. If a state backend fails, the protected call does not run, and Reqtide does not invent fallback behavior. A failing observer or bookkeeping step after the call never replaces your result.
  • Time is monotonic and integer-based. A clock that steps backwards never refills a quota.
  • State is bounded. At its ceiling a memory backend frees expired records first. Then it evicts the oldest bookkeeping that protects nothing. Then it rejects new partitions. It never evicts an active permit, a budget balance or an open circuit.

Separate packages and plans

ItemState
Shared state in Redis for rate limit, circuit, bulkhead, retry budget and cooldown (enaxon/reqtide-redis)Beta, on Packagist. Guide: Shared state with Redis. Run in Docker and in CI with PHP 8.3 to 8.5 on the command line, with Redis 7.4.11 and 8.2.10. Sentinel, Cluster and Valkey are not supported; PHP-FPM has the CI lane named above.
Guzzle integration (enaxon/reqtide-guzzle)Beta, on Packagist. Guide: Guzzle requests with Reqtide. Run in Docker and in CI with PHP 8.3 to 8.5 on the command line and Guzzle 7.15.5. PSR-18 is not supported, and the client was not run under PHP-FPM.
PSR-18, PSR-15, Symfony HTTP and framework registrationPossible later
The separate packages and the plans around them.

Each planned item needs its own design and tests first.

Versioning and compatibility

  • Semantic versioning. Before 1.0 a minor version can change the API. Every break is listed in CHANGELOG.md (opens in a new tab). From 1.0 only a major version breaks it.
  • One version for the three packages. enaxon/reqtide, enaxon/reqtide-redis and enaxon/reqtide-guzzle are released together. Each adapter requires exactly the same version of the core.
  • What the promise covers. Public classes, interfaces, enums, constants and methods that are not marked @internal. It also covers the string values of ReasonCode and the members of the Problem JSON, including its type identifier. A new reason code is not a break, so handle unknown codes.
  • What it does not cover. Anything marked @internal, the internals of the test helpers, the Redis key layout and the Lua scripts, and the text of messages. Reqtide checks the registry schema version and refuses a mismatch.
  • Do not extend. Classes are final. The two abstract exception bases are for catching, not for extending.