Skip to content

[13.x] Memoize credentials in SqsConnector#59866

Merged
taylorotwell merged 1 commit into
laravel:13.xfrom
kieranbrown:sqs-memoize-credential-provider
Apr 26, 2026
Merged

[13.x] Memoize credentials in SqsConnector#59866
taylorotwell merged 1 commit into
laravel:13.xfrom
kieranbrown:sqs-memoize-credential-provider

Conversation

@kieranbrown
Copy link
Copy Markdown
Member

Summary

When a queue config sets credentials.provider = ecs (or instance) — as Laravel Cloud does for managed queue workers via Illuminate\Foundation\Cloud::configureManagedQueues()SqsConnector::resolveCredentialProvider() returned a raw Aws\Credentials\EcsCredentialProvider / Aws\Credentials\InstanceProfileProvider instance.

The AWS SDK's Aws\ClientResolver::_apply_credentials() short-circuits any callable passed as credentials (the SDK only auto-wraps in memoize() for its own internal defaultProvider() path), and the signer middleware (Aws\Middleware::signer) invokes the credentials provider on every signed request. Combined with the fact that EcsCredentialProvider / InstanceProfileProvider issue a fresh HTTP GET to the EKS Pod Identity Agent / EC2 metadata endpoint on every __invoke(), the result is that every single SQS API call (ReceiveMessage, DeleteMessage, ChangeMessageVisibility, …) triggers an HTTP fetch to the credentials endpoint.

For a long-lived queue:work worker on Laravel Cloud doing 60 long-polls/minute × N workers per node, this saturates the EKS Pod Identity Agent's built-in rate limiter and starts producing HTTP 429 responses, which the SDK currently does not retry (separate fix in aws/aws-sdk-php#3279).

What this PR does

Wraps the resolved provider in Aws\Credentials\CredentialProvider::memoize() so credentials are cached in-process for the lifetime of the worker, with the SDK's standard 60-second pre-expiry refresh window. This matches what the SDK's own CredentialProvider::defaultProvider() does for the auto-discovered chain (which also wraps ecsCredentials() in memoize()), so it's not a behavioral departure from SDK norms — it just brings the explicit-provider path into line with the implicit one.

The match statement was lifted into a local variable so memoize() wraps both branches (and any future ones) without duplication.

$resolved = match ($provider) {
    'ecs' => CredentialProvider::ecsCredentials($options),
    'instance' => CredentialProvider::instanceProfile($options),
    default => throw new InvalidArgumentException(
        \"Invalid credential provider [{$provider}].\"
    ),
};

return CredentialProvider::memoize($resolved);

Benefit to end users

  • Queue workers using ECS / EKS Pod Identity credentials make one credentials fetch per worker per ~6 hours instead of one per signed SQS request.
  • Drastically reduces request volume to the Pod Identity Agent, eliminating 429 throttling under steady-state load.
  • Same fix applies to the instance (EC2 IMDS) branch.
  • No config change required — the fix applies automatically to anyone using credentials.provider = ecs or credentials.provider = instance.

Backwards compatibility

  • No public API change. Return type of resolveCredentialProvider() remains callable|null.
  • CredentialProvider::memoize() is part of the AWS SDK's public, documented API and has been since the SDK's early 3.x releases.
  • Aws\Credentials\Credentials always carries an Expiration, so memoize() never falls into the $isConstant = true branch and refreshes correctly.
  • Per-process scope: each worker has its own static $result cache (verified independently — PHP scopes static per Closure instance, not per literal). No cross-worker state, no shared registry, no risk of cred leakage.

Test plan

  • tests/Queue/ suite passes locally (207 tests, 558 assertions).
  • No dedicated SqsConnectorTest exists in the codebase — the connector has historically been tested via integration only. Happy to add a structural regression test (asserting the returned callable is a Closure rather than a raw provider instance) if maintainers want one.

Generated by AI tools (Claude), and reviewed by Kieran Brown.

When a queue config sets `credentials.provider = ecs` (or `instance`),
SqsConnector::resolveCredentialProvider returned a raw EcsCredentialProvider
or InstanceProfileProvider. The AWS SDK's ClientResolver short-circuits any
callable passed as `credentials` (no automatic memoize wrap), and the signer
middleware invokes the provider on every signed request — so every SQS API
call triggered a fresh HTTP fetch to the EKS Pod Identity Agent / EC2
metadata endpoint.

Wrap the resolved provider in CredentialProvider::memoize so credentials are
cached in-process for the lifetime of the worker, with the SDK's standard
60-second pre-expiry refresh window. This matches what the SDK's own
defaultProvider() does and stops queue workers from saturating the Pod
Identity Agent's rate limiter under steady-state polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kieranbrown added a commit to kieranbrown/cloud-managed-queues-demo that referenced this pull request Apr 26, 2026
Tests the SqsConnector memoize fix that wraps the resolved ECS / instance
credentials provider in CredentialProvider::memoize() so credentials are
cached in-process instead of fetched per signed request.

Upstream PR: laravel/framework#59866
@taylorotwell taylorotwell merged commit 98e9968 into laravel:13.x Apr 26, 2026
52 of 54 checks passed
jonagoldman pushed a commit to deplox/laravel-framework that referenced this pull request Apr 30, 2026
When a queue config sets `credentials.provider = ecs` (or `instance`),
SqsConnector::resolveCredentialProvider returned a raw EcsCredentialProvider
or InstanceProfileProvider. The AWS SDK's ClientResolver short-circuits any
callable passed as `credentials` (no automatic memoize wrap), and the signer
middleware invokes the provider on every signed request — so every SQS API
call triggered a fresh HTTP fetch to the EKS Pod Identity Agent / EC2
metadata endpoint.

Wrap the resolved provider in CredentialProvider::memoize so credentials are
cached in-process for the lifetime of the worker, with the SDK's standard
60-second pre-expiry refresh window. This matches what the SDK's own
defaultProvider() does and stops queue workers from saturating the Pod
Identity Agent's rate limiter under steady-state polling.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants