Skip to content

fix(redis): cache GCP IAM token to prevent async event loop blocking - #26317

Closed
MaximeBOUDIER wants to merge 1 commit into
BerriAI:mainfrom
MaximeBOUDIER:fix/gcp-iam-redis-token-caching
Closed

fix(redis): cache GCP IAM token to prevent async event loop blocking#26317
MaximeBOUDIER wants to merge 1 commit into
BerriAI:mainfrom
MaximeBOUDIER:fix/gcp-iam-redis-token-caching

Conversation

@MaximeBOUDIER

Copy link
Copy Markdown

Description

GCPIAMCredentialProvider.get_credentials() calls _generate_gcp_iam_access_token on every Redis connection establishment with no caching. This function performs synchronous HTTP (GCP metadata server) and gRPC (IAMCredentials/GenerateAccessToken) calls which block Python's asyncio event loop.

Under concurrent load — connection pool warm-up, parallel health checks, rate limit tracking — multiple connections are established simultaneously, each triggering an independent blocking IAM token refresh. These refreshes serialise inside the single-threaded event loop, causing individual Redis spans to take 20-25 seconds instead of milliseconds.

Observed in production via Datadog APM: a single INCRBYFLOAT Redis span took 25.6s (90% of a 28.4s trace), with GCP metadata + GenerateAccessToken gRPC calls visible inside the span. This cascaded into aiohttp.SocketTimeoutError on upstream LLM API calls — not because the upstream was slow, but because the event loop was frozen and the 30s sock_read timer fired on a connection that was never given CPU time.

Note: PR #24426 correctly introduced get_credentials_async() with asyncio.to_thread, but get_credentials() (called by redis-py on connection establishment) still has no caching and blocks on every call.

Fix

Add a module-level token cache (dict keyed by service account → (token, expiry_monotonic)).

_get_cached_gcp_iam_token() returns the cached token on cache hit (zero I/O), and refreshes only when expired using double-checked locking so only one thread performs the network round-trip at expiry time.

  • GCP IAM tokens are valid for 1 hour; TTL is set to 55 minutes to refresh safely before expiry
  • Cache is shared across all GCPIAMCredentialProvider instances for the same service account — N concurrent Redis connections on the same pod share one token and avoid N concurrent blocking refreshes
  • get_credentials_async() is updated to call _get_cached_gcp_iam_token via asyncio.to_thread so it also benefits from caching

Tests

  • Updated existing test that assumed a fresh token on every call to reflect caching behaviour
  • Added: cache hit (no redundant I/O), cache expiry + refresh, cache sharing across instances
  • Added autouse fixture to isolate the module-level cache between tests

Checklist

  • Tests added in tests/test_litellm/
  • make test-unit passes (1 pre-existing unrelated failure in test_sync_client_preserves_password_for_cluster_when_url_also_set — present on main before this PR)

This pull request was created with AI assistance.

## Problem

GCPIAMCredentialProvider.get_credentials() calls _generate_gcp_iam_access_token
on every Redis connection establishment. This function performs synchronous HTTP
and gRPC calls (google-auth + google-cloud-iam) which block Python's asyncio
event loop while running.

Under concurrent load (e.g. connection pool warm-up, parallel health checks),
multiple connections are established simultaneously, each triggering an
independent blocking IAM token refresh. These refreshes serialise behind each
other inside the single-threaded event loop, causing individual Redis spans to
take 20-25 seconds instead of milliseconds.

Observed in production via Datadog APM: a single INCRBYFLOAT Redis span took
25.6 seconds (90% of a 28.4s trace), with GCP metadata + GenerateAccessToken
gRPC calls visible inside the span. This cascaded into aiohttp SocketTimeoutError
on upstream LLM API calls — not because the upstream was slow, but because the
event loop was frozen and the 30-second sock_read timer fired on a connection
that was never given CPU time.

## Fix

Add a module-level token cache (dict keyed by service account, value is
(token, expiry_monotonic)). _get_cached_gcp_iam_token() returns the cached
token on cache hit (no I/O), and refreshes only when expired using
double-checked locking so only one thread performs the network round-trip.

GCP IAM tokens are valid for 1 hour; the cache TTL is set to 55 minutes
(_GCP_IAM_TOKEN_TTL_SECONDS = 3300) to refresh safely before expiry.

The cache is shared across all GCPIAMCredentialProvider instances for the same
service account, so N concurrent Redis connections on the same pod share a
single token and avoid N concurrent blocking refreshes.

get_credentials_async() already used asyncio.to_thread (non-blocking), and is
updated to call _get_cached_gcp_iam_token so it also benefits from caching.

## Tests

- Updated existing test that expected a fresh token on every call to reflect
  the new caching behaviour.
- Added tests for: cache hit (no redundant I/O), cache expiry and refresh,
  and cache sharing across multiple provider instances.
- Added autouse fixture to clear the module-level cache between tests.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@veria-ai

veria-ai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Low: No security issues found

This PR adds a thread-safe module-level cache for GCP IAM tokens used in Redis authentication. The caching uses proper double-checked locking, the cache key is a server-side configuration value (not user input), and the TTL provides adequate margin before token expiry. No new attack surface introduced.


Status: 0 open
Risk: 1/10

Posted by Veria AI · 2026-04-23T09:04:00.513Z

@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes event-loop blocking in GCPIAMCredentialProvider.get_credentials() by introducing a module-level token cache with double-checked locking, so concurrent Redis connection establishments share a single IAM token instead of each triggering a blocking network round-trip. The implementation is correct, the TTL of 55 minutes is appropriate for 1-hour GCP IAM tokens, and the new tests cover cache hits, expiry refresh, and cross-instance sharing.

Confidence Score: 5/5

Safe to merge — the caching logic is correct and well-tested; only minor style issues remain.

All findings are P2 (unused import, stale docstring). The double-checked locking pattern is correct, thread-safe under CPython's GIL, and tests adequately cover the new caching contract including expiry and cross-instance sharing.

No files require special attention.

Important Files Changed

Filename Overview
litellm/_redis_credential_provider.py Adds module-level token cache with double-checked locking for GCP IAM tokens; logic is correct with two minor style issues (unused Optional import, stale docstring).
tests/test_litellm/test_redis.py Updates and adds tests for the new caching behavior (cache hit, expiry, cross-instance sharing) with an autouse fixture to isolate module-level state; looks good.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[get_credentials / get_credentials_async] --> B{Cache hit?\n_token_cache.get}
    B -- Yes, not expired --> C[Return cached token\nzero I/O]
    B -- No / expired --> D[Acquire _token_cache_lock]
    D --> E{Double-check:\nstill expired?}
    E -- No, another thread refreshed --> F[Return fresh token\nfrom cache]
    E -- Yes --> G[_generate_gcp_iam_access_token\nHTTP + gRPC to GCP]
    G --> H[Store token + expiry\nmonotonic + 3300s]
    H --> I[Release lock\nReturn token]
Loading

Reviews (1): Last reviewed commit: "fix(redis): cache GCP IAM token to preve..." | Re-trigger Greptile

from typing import Tuple
import threading
import time
from typing import Dict, Optional, Tuple

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused Optional import

Optional is imported on line 4 but never referenced anywhere in the file.

Suggested change
from typing import Dict, Optional, Tuple
from typing import Dict, Tuple

Comment on lines 82 to +87
"""
redis.credentials.CredentialProvider implementation that generates a fresh GCP IAM
token on every new connection. This fixes the 1-hour token expiry issue for async
Redis cluster clients, which previously generated the token once at startup and
cached it as a static password.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale docstring contradicts the new caching behavior

The first two sentences still say "generates a fresh GCP IAM token on every new connection," which directly contradicts the caching logic introduced by this PR. The class-level paragraph added below partially corrects this but leaves the opening claim misleading for future readers.

@codspeed-hq

codspeed-hq Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing MaximeBOUDIER:fix/gcp-iam-redis-token-caching (5e61737) with main (e9e86ed)

Open in CodSpeed

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/_redis_credential_provider.py 95.45% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

from typing import Tuple
import threading
import time
from typing import Dict, Optional, Tuple
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

@github-actions github-actions Bot added the stale label Jul 23, 2026
@github-actions github-actions Bot closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants