Problem
In a production Kubernetes deployment the ONNX Runtime inference service intermittently fails to load models stored in cloud object storage (e.g., Amazon S3, Google Cloud Storage). The failure manifests as a model‑load timeout followed by inference request timeouts. The issue appears only after a period of sustained traffic and disappears after a pod restart.
Observed Symptoms
- ONNX Runtime logs show
ModelLoadError: timeout while downloading model from S3. - Pod logs contain repeated
503 Service Unavailableerrors from the S3 SDK. - CPU and memory usage remain normal; the container is not OOM‑killed.
- Metrics:
onnxruntime_model_load_duration_secondsspikes to >30 s. - After a pod restart the model loads successfully and inference latency returns to baseline.
Sample Log Excerpts
2026-09-22T12:34:56.123Z INFO ModelLoader - Starting download of s3://ml-models/v1/model.onnx
2026-09-22T12:35:06.789Z ERROR ModelLoader - ModelLoadError: timeout after 10s while fetching from S3
2026-09-22T12:35:06.790Z WARN S3Client - Received 429 TooManyRequests from s3.amazonaws.com
2026-09-22T12:35:07.001Z INFO HealthCheck - Inference unavailable: model not loaded
Root Cause
The ONNX Runtime container downloads the model directly from S3 at startup using the AWS SDK. During traffic spikes the Kubernetes pod makes many concurrent model‑load attempts (e.g., during rolling updates or when the pod restarts after a crash). The cloud provider enforces API request throttling (HTTP 429) when the request rate exceeds the allocated quota. Because the SDK does not retry with exponential backoff by default for GetObject calls, the download aborts after the client‑side timeout, leaving the runtime in a failed state.
Key contributing factors:
- Unbounded parallel downloads: Each pod attempts to download the full model file (~200 MiB) on every start, regardless of whether the model is already cached on the node.
- Insufficient S3 request quota: The default bucket request rate limit (≈3,500 GET requests / second per prefix) is exceeded during coordinated rollouts.
- Missing retry logic: The SDK call is wrapped in a single
GetObjectrequest without retry policy, so a transient 429 error results in immediate failure.
Debug
The following steps were used to confirm the root cause.
1. Inspect S3 request metrics
aws cloudwatch get-metric-statistics \
--namespace AWS/S3 \
--metric-name 4xxErrors \
--dimensions Name=BucketName,Value=ml-models Name=StorageType,Value=AllStorageTypes \
--statistics Sum \
--period 60 \
--start-time 2026-09-22T12:00:00Z \
--end-time 2026-09-22T13:00:00Z
Result: spikes of 4xxErrors aligning with pod restarts.
2. Capture SDK retry configuration
import boto3
from botocore.config import Config
cfg = Config(retries={'max_attempts': 3, 'mode': 'standard'})
s3 = boto3.client('s3', config=cfg)
The existing code used the default config, which sets max_attempts to 1 for GetObject.
3. Verify pod concurrency during rollout
kubectl get pods -n ml-inference -l app=onnx-runtime -o wide
kubectl describe deployment onnx-runtime -n ml-inference
During a rolling update 5 pods started simultaneously, each triggering a model download.
4. Network trace (optional)
kubectl exec -n ml-inference onnx-runtime-abc123 -- tcpdump -i any -w /tmp/capture.pcap host s3.amazonaws.com
Analysis of the pcap showed multiple concurrent GET /ml-models/v1/model.onnx requests with overlapping TCP retransmissions.
Solution
Two complementary mitigations were applied:
1. Add exponential backoff and retry to the S3 download code
Wrap the model fetch in a retry loop with jitter. This handles transient 429 responses without aborting the startup.
Before
def download_model(bucket, key, dest_path):
s3 = boto3.client('s3')
s3.download_file(bucket, key, dest_path) # single attempt, default timeout
After
import time
import random
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
RETRY_MAX = 5
BASE_DELAY = 1 # seconds
def download_model(bucket, key, dest_path):
cfg = Config(
retries={'max_attempts': RETRY_MAX, 'mode': 'standard'},
connect_timeout=5,
read_timeout=30,
)
s3 = boto3.client('s3', config=cfg)
for attempt in range(1, RETRY_MAX + 1):
try:
s3.download_file(bucket, key, dest_path)
return
except ClientError as e:
if e.response['Error']['Code'] in ('Throttling', 'TooManyRequests'):
delay = BASE_DELAY * (2 ** (attempt - 1)) + random.uniform(0, 0.5)
print(f"Retry {attempt}/{RETRY_MAX} after {delay:.2f}s due to {e.response['Error']['Code']}")
time.sleep(delay)
continue
raise
raise RuntimeError("Failed to download model after retries")
2. Cache the model on the node to avoid repeated downloads
Introduce an initContainer that pulls the model once to a shared emptyDir volume. Subsequent containers mount the same volume, eliminating S3 traffic on pod restarts.
Before (single container download)
containers:
- name: onnx-runtime
image: myrepo/onnx-runtime:latest
env:
- name: MODEL_S3_URI
value: s3://ml-models/v1/model.onnx
After (initContainer cache)
volumes:
- name: model-cache
emptyDir: {}
initContainers:
- name: model-fetcher
image: amazon/aws-cli:2.13.0
command: ["sh", "-c"]
args:
- |
aws s3 cp s3://ml-models/v1/model.onnx /model/model.onnx \
--no-progress --retry-mode standard --max-attempts 5
envFrom:
- secretRef:
name: aws-credentials
volumeMounts:
- name: model-cache
mountPath: /model
containers:
- name: onnx-runtime
image: myrepo/onnx-runtime:latest
env:
- name: MODEL_PATH
value: /model/model.onnx
volumeMounts:
- name: model-cache
mountPath: /model
3. Increase S3 request quota (optional)
Request a higher request rate limit from the cloud provider or enable request rate optimization by using a random prefix in the object key (e.g., v1/ab12/model.onnx).
Verify
After applying the changes, perform the following checks.
1. Observe pod startup logs
2026-09-22T14:02:01.112Z INFO ModelLoader - Model found in /model/model.onnx, skipping download
2026-09-22T14:02:01.115Z INFO HealthCheck - Inference ready
2. Confirm no 429 errors in CloudWatch
aws cloudwatch get-metric-statistics \
--namespace AWS/S3 \
--metric-name 4xxErrors \
--statistics Sum \
--period 60 \
--start-time 2026-09-22T14:00:00Z \
--end-time 2026-09-22T15:00:00Z
Result: Sum = 0 for the monitored period.
3. Validate inference latency
curl -s http://onnx-runtime.ml.svc.cluster.local/predict -d '{"input": [1,2,3]}' | jq .
Latency returns to ~15 ms per request, matching pre‑incident baseline.
4. Check retry metrics
kubectl logs -l app=onnx-runtime -c onnx-runtime | grep "Retry"
Only occasional retries observed during a controlled load test, confirming backoff works.
Prevent
- Cache models locally: Use init containers or sidecars to pull artifacts once per node.
- Enable SDK retries: Configure
max_attemptsandmode=standardfor all S3 interactions. - Apply exponential backoff with jitter to avoid thundering‑herd effects during rollouts.
- Monitor S3 throttling metrics (4xx errors,
ThrottlingException) and set alerts. - Rate‑limit pod restarts: Use
maxSurgeandmaxUnavailablesettings in the Deployment to stagger rollouts. - Use bucket prefixes or request‑rate‑optimized storage classes when high concurrency is expected.
Related Topic Hub: Model Serving Troubleshooting Hub
FAQ
- Why does the model load succeed after a pod restart but fail during a rolling update?
The restart triggers a single download, while a rolling update may start multiple pods simultaneously, exceeding the S3 request rate limit and causing 429 throttling.
- Can I rely on the default AWS SDK retry policy?
No. For
GetObjectthe defaultmax_attemptsis 1. Explicitly set a higher retry count and usemode=standardto enable exponential backoff. - Is increasing the S3 request quota always safe?
It mitigates throttling but does not address the underlying thundering‑herd pattern. Combining quota increase with caching and backoff yields a more robust solution.
- How do I verify that the init container has successfully cached the model?
Check the init container exit code (should be 0) and list the file inside the shared volume:
kubectl exec.-- ls -l /model/model.onnx - What if the model size is >1 GiB and does not fit in an emptyDir?
Use a
hostPathvolume backed by a node‑local SSD or a persistent volume with sufficient capacity, and mount it read‑only for the inference container.