Skip to content

RetryMiddleware calculates exponential backoff but never calls sleep() (Immediate Retry/No Delay) #673

Description

@Jony-Shark

Bug description

The RetryMiddleware in google/gax (PHP) calculates the exponential backoff delay correctly via initialRetryDelayMillis, but never actually delays the execution (e.g., via usleep or sleep) before firing the next request. This causes the SDK to immediately fire all retry attempts in rapid succession (within milliseconds), defeating the entire purpose of exponential backoff. This frequently leads to severe Rate Limiting penalties (like Code 8: RESOURCE_EXHAUSTED) or WAF blocks.

Environment

  • OS: Linux 7.0.10
  • PHP version: 8.3.31
  • Package name and version: cloud-ai-platform:v1.13.0

Steps to reproduce

Configure a PredictionServiceClient (or any GAPIC client) with custom RetrySettings.
Mock the network to return a 429 Too Many Requests (or throw an ApiException with Code 8 / RESOURCE_EXHAUSTED).
Set initialRetryDelayMillis to a high value (e.g., 5000 / 5 seconds) and set max retries.
Execute the call.
Notice that all retries fail and the script terminates in < 1 second instead of taking the mathematically expected time.

Root cause analysis

Looking at src/Middleware/RetryMiddleware.php: Around the retry() method:

$delayMs = min($delayMs * $delayMult, $maxDelayMs);
        $nextHandler = new RetryMiddleware(
            $this->nextHandler,
            $this->retrySettings->with([
                'initialRetryDelayMillis' => $delayMs,
            ]),
            $deadlineMs,
            $this->retryAttempts + 1
        );
        // Set the timeout for the call
        $options['timeoutMillis'] = $timeoutMs;
        return $nextHandler(
            $call,
            $options
        );

The $delayMs is calculated and stored in the new RetryMiddleware instance's RetrySettings, but the actual delay/sleep is never executed anywhere in the middleware chain before $nextHandler($call, $options) invokes the HTTP/gRPC transport layer.

Expected behavior

The SDK should pause execution (e.g., via usleep($delayMs * 1000)) or schedule the promise resolution to respect the initialRetryDelayMillis before dispatching the next request to the server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions