Problem: ChromaDB Token Refresh Failure During Live Data Ingestion
In a production streaming pipeline that continuously upserts embedded documents into a hosted ChromaDB instance, the following symptoms were observed after roughly one hour of operation:
- WebSocket connections dropped with
connection closed with code 4001 (authentication_failed). - HTTP
401 Unauthorizedresponses on/upsertand/addendpoints, e.g.:HTTP/1.1 401 Unauthorized Content-Type: application/json { "detail": "Token expired" } - gRPC calls returned
UNAUTHENTICATEDerrors:grpc.UNAUTHENTICATED: token_refresh_error: failed to obtain new token - Backlog accumulation – a pipeline ingesting 5 k embeddings / sec built a queue of ~360 k records during a 12‑minute outage (AWS Lambda incident).
These failures interrupt the low‑latency guarantee required for real‑time event processing and cause downstream SLA breaches.
Root Cause Analysis
Token Lifecycle in ChromaDB
According to the ChromaDB Authentication Guide, a client obtains an access token via the /auth/token endpoint. The token has a configurable TTL (default 1 hour) and must be refreshed using the /auth/refresh endpoint before expiry. The SDK injects the refreshed token into the Authorization: Bearer <token> header for subsequent HTTP, gRPC, and WebSocket requests.
Observed Failure Path
- At
T+60 minthe access token reaches its TTL. - The SDK invokes the refresh endpoint. In many production environments the refresh request is throttled or returns
403 Forbidden(“Token refresh rejected”) – see GitHub Issue #1389 and the Kubernetes incident where the auth provider throttled the request. - The SDK retries the refresh according to an exponential back‑off policy, eventually exhausting its
max_retrycount and raisingTokenRefreshError: Maximum retry attempts exceeded while refreshing access token(GitHub Issue #1245). - Because the SDK does not receive a new token, it continues to use the stale token for existing WebSocket connections. The server validates the token on each frame and closes the socket with code
4001 (authentication_failed). - All subsequent HTTP/gRPC upsert calls carry the expired token, resulting in
401 UnauthorizedorUNAUTHENTICATEDerrors.
The core reasons are:
- Refresh endpoint failure due to throttling, mis‑configured OIDC client, or server‑side 500 errors (on‑premise incident).
- SDK bug where the refreshed token is not propagated to an already‑opened WebSocket (GitHub Issue #1389).
- Insufficient retry/back‑off configuration causing the client to give up before the auth provider recovers.
Investigation and Debugging Steps
1. Capture Authentication‑Related Logs
2024-07-15T12:00:58.123Z INFO chromadb.client: Refresh token request sent
2024-07-15T12:00:58.456Z WARN chromadb.client: Refresh token response 403 Forbidden
2024-07-15T12:01:00.001Z ERROR chromadb.client: TokenRefreshError: Maximum retry attempts exceeded
2024-07-15T12:01:02.789Z WARN websocket: connection closed with code 4001 (authentication_failed)
2024-07-15T12:01:03.112Z ERROR http: POST /upsert 401 Unauthorized
2. Verify Token TTL and Refresh Endpoint Directly
# Inspect current token expiry (JWT payload)
jwt decode $(cat /var/run/chroma/access_token.jwt) | jq .exp
# Manually invoke refresh endpoint
curl -X POST https://chroma.example.com/auth/refresh \
-H "Authorization: Bearer $(cat /var/run/chroma/refresh_token.jwt)" \
-d '{"grant_type":"refresh_token"}' -i
Expected successful response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600
}
3. Inspect WebSocket Metadata Propagation
# Enable SDK debug mode
export CHROMADB_DEBUG=1
# Observe the token used for each frame (client logs)
2024-07-15T12:01:01.234Z DEBUG websocket: sending frame with Authorization: Bearer
4. Check Rate‑Limit Headers from Auth Provider
curl -I -X POST https://auth.example.com/token \
-d 'client_id=xyz&client_secret=abc&grant_type=client_credentials'
# Look for:
# X-RateLimit-Remaining: 0
# Retry-After: 30
5. Validate SDK Version
GitHub Issue #1245 identifies the bug in chromadb==0.3.7. Verify the version in use:
pip show chromadb
Name: chromadb
Version: 0.3.7
Resolution
1. Upgrade the SDK to a Version with Fixed Refresh Logic
Version 0.3.9 includes:
- Propagation of refreshed tokens to active WebSocket connections.
- Configurable
refresh_retry_maxandrefresh_backoff_baseparameters.
# Before
pip install "chromadb==0.3.7"
# After
pip install "chromadb==0.3.9"
2. Extend Token TTL or Use Long‑Lived Service Accounts
Modify the deployment configuration (see Security Best Practices) to set access_token_ttl = 86400 (24 h) for service‑to‑service ingestion.
# chroma_config.yaml
auth:
token_ttl_seconds: 86400
refresh_token_ttl_seconds: 2592000
3. Adjust Refresh Retry Policy
Provide explicit retry settings when constructing the client:
from chromadb import Client
client = Client(
api_url="https://chroma.example.com",
auth_token="initial-access-token",
refresh_token="initial-refresh-token",
refresh_retry_max=10,
refresh_backoff_base=2, # exponential back‑off: 2,4,8,...
)
4. Implement a Guarded Refresh Callback (Workaround for OIDC Throttling)
If the auth provider imposes strict rate limits, introduce a local cache that reuses a successfully refreshed token for up to retry_window_seconds before attempting another refresh.
import time
from threading import Lock
class CachedRefresher:
def __init__(self, client, cache_ttl=300):
self.client = client
self.cache_ttl = cache_ttl
self._lock = Lock()
self._cached_token = None
self._expires_at = 0
def get_token(self):
with self._lock:
now = time.time()
if self._cached_token and now < self._expires_at:
return self._cached_token
# perform refresh
resp = self.client.refresh_token()
self._cached_token = resp["access_token"]
self._expires_at = now + resp["expires_in"] - 30 # safety margin
return self._cached_token
# usage
refresher = CachedRefresher(client)
client.set_auth_callback(refresher.get_token)
Verification
- Token Refresh Success
2024-07-15T13:00:01.112Z INFO chromadb.client: Refresh token request sent 2024-07-15T13:00:01.345Z INFO chromadb.client: Refresh token succeeded, new expires_in=3600 - WebSocket Remains Open
2024-07-15T13:00:02.001Z DEBUG websocket: connection authenticated with new token 2024-07-15T13:00:02.050Z INFO websocket: ping/pong healthy - Upsert Calls Return 200
2024-07-15T13:00:03.210Z INFO http: POST /upsert 200 OK - Metrics – In Grafana, the
chroma_ingest_success_totalcounter should show no drops, and thechroma_token_refresh_failure_totalmetric should be zero.
Prevention and Best Practices
- Monitor Token Health – Export
chroma_token_expiry_secondsand set an alert when remaining TTL < 300 s. - Centralize Secret Management – Store refresh tokens in AWS Secrets Manager or HashiCorp Vault and rotate them with a cadence shorter than the access token TTL.
- Use Service Accounts with Long‑Lived Tokens when possible; avoid per‑request user tokens for high‑throughput ingestion.
- Configure Auth Provider Rate Limits to accommodate bursty refresh traffic (e.g., allow 5 requests per minute per client ID).
- Enable SDK Debug Logging in Staging to catch token propagation bugs before production rollout.
- Graceful Shutdown Hook – On process termination, close WebSocket connections after sending a final
closeframe with a fresh token to avoid “authentication_failed” logs.
Related Topic Hub: Vector Databases Troubleshooting Hub
FAQ
- Why does the token refresh succeed when I call the endpoint manually but fail inside the SDK?
The SDK in versions prior to
0.3.9does not correctly handleRetry-Afterheaders and aborts after the defaultmax_retrycount. Upgrading the SDK or configuringrefresh_retry_maxresolves the issue. - Can I rely on the refresh token indefinitely?
Refresh tokens also have a TTL (default 30 days). Monitor
refresh_token_expiry_secondsand rotate the underlying OIDC client secret before it expires. - My WebSocket stays open after the access token expires – is that expected?
WebSocket servers validate the token on each frame. If the server is mis‑configured to skip re‑validation, the connection may appear stable but will eventually be closed when the client sends a frame with an expired token. Ensure the server follows the spec described in the Streaming Client Documentation.
- How do I differentiate between a 401 caused by an expired token versus an insufficient scope?
Check the error body. An expired token returns
"Token expired", while scope issues return"Insufficient scopes for operation". TheWWW-Authenticateheader also includeserror="invalid_token"for expiry anderror="insufficient_scope"for scope problems. - Is exponential back‑off safe for token refresh, or can it cause longer outages?
Exponential back‑off reduces load on the auth provider but can increase the window of failure. Pair it with a
max_retrythat is high enough to survive transient throttling (e.g., 10 attempts) and a safety margin in the access token TTL (e.g., refresh 5 minutes before expiry).