Skip to the guide
All guides

Choosing policies

See which protection answers which problem, how your classifier decides what counts as a failure, and which options you must size yourself.

app Reqtide PHP 0.1.0checked reading 5 minutes

On this page

Reqtide ships no defaults for timeouts, limits, windows or delays, and these guides recommend no numbers. The right values depend on your dependency, your traffic and your risk.

Every code block in the protection guides comes from examples/09-choosing-policies.php (opens in a new tab) or a sibling example. A script runs them and compares them with the recorded output. The numbers make each protection visible in a few lines. They are not advice.

All limits apply inside one PHP process. Under PHP-FPM, a pipeline built inside a request starts every request fresh. See Limits.

Which problem, which policy

ProblemPolicyBuilder callGuide
One slow call holds the caller for too longTotal deadline, plus an attempt timeoutnewContext(timeout) and attemptTimeout(), both requiredConcepts
Brief failures that a second try may fixRetry with backoff, paid from a retry budgetretry(), retryBudget()Retry and budget
A provider quota must not be exceededRate limitrateLimit()Rate limiting
A dependency is down and callers keep hitting itCircuit breakercircuitBreaker()Circuit breaker
One slow dependency uses up all concurrent work in the processBulkheadbulkhead()Bulkhead and cooldown
The server said "retry after N seconds"Retry-After as a lower bound, plus a shared cooldownExceptionClassifier::retryAfterFrom(), cooldown()Retry and budget, Bulkhead and cooldown
Callers need something useful when the call failsFallbackfallbackScope() and a fallback in newContext()Fallback
Problems, the policy that answers each, and where to read more.

You pay only for what you configure. A policy exists only if you call its method.

The classifier decides what counts

Policies act on the outcome of each attempt, and your classifier decides what an outcome means. ExceptionClassifier starts conservative:

  • A returned value is a success.
  • An exception is not retried and not replaced by a fallback unless you list its class with retryOn() or fallbackOn().
  • Every exception counts as a circuit failure. You can list a class in ignoreForCircuit(), for example the caller's own validation error, or narrow the failures with countAsCircuitFailure().

unacceptableValuesWhen() marks a returned value as a failure. Such a value is a circuit failure, is never retried and is never replaced. retryAfterFrom() extracts a wait from an exception. The decision table is in the docblock of ExceptionClassifier (opens in a new tab).

Rate-limit, circuit, bulkhead and cooldown refusals, cancellations and storage errors are neutral. They never count as dependency failures.

Reqtide does not retry an exception nobody listed, because repeating an unexpected failure is a decision you must make explicitly. It still counts the exception as a circuit failure. A circuit that ignored unknown failures would stay closed against a dependency that fails in a way nobody anticipated. Matching uses instanceof, so a listed parent class or interface covers its subclasses.

Every snippet starts the same way

The protection guides build their pipelines from two required options. The attempt timeout is required. Cooperative means your code respects the time, because the synchronous runtime cannot stop it. The deadline you pass to newContext() is a separate, total budget for the whole operation.

PHP
/** Every pipeline below starts from the same two required options. */
$base = static fn(): PipelineBuilder => PipelineBuilder::profile('choosing', 'v1')
    ->partition('shop', 'prod', 'catalog-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative);

$ok = static fn(): string => 'ok';
$context = static fn(CompiledPipeline $pipeline): ExecutionContext => $pipeline->newContext(Duration::seconds(30));

What is checked, in which order

For each attempt, Reqtide checks the deadline and the context first. Then it checks retry eligibility, the minimum remaining allowance and the retry budget reservation. The last two apply only to attempts after the first. Next come the circuit, the bulkhead, the shared cooldown and the rate limit, and then the dispatch. If a later check refuses, Reqtide releases the earlier permits. Nothing is held during a retry delay. Concepts shows the order as a table.

Sizing checklist

Reqtide checks the combination when you compile, and its messages name the option. These are the constraints it enforces. They are not recommended values.

  • Every lease must exceed the attempt timeout by more than 1 second. This covers the bulkhead lease, the circuit permit lease and the retry budget reservation lease.
  • retry() with more than one attempt needs retryBudget(). A retryBudget() without retry() is rejected.
  • The circuit minimum samples cannot exceed a count window. The probe success threshold cannot exceed the probe capacity.
  • fallbackScope(IncludeAdmissionRefusals) needs at least one admission policy.

Everything else is a decision about your dependency: limits, windows, delays and pool sizes. Measure it, write the values down, and test the pipeline under virtual time (Testing with the virtual clock).

One guide per protection