Kubernetes Cron Overlap Causing Concurrent GPT‑3.5 Batch Jobs
Problem
The nightly batch pipeline that invokes the OpenAI gpt‑3.5‑turbo model is implemented as a CronJob on a Kubernetes cluster. During several nights the pipeline failed with errors similar to:
2024-10-12T02:15:43Z ERROR rate limit exceeded for model gpt-3.5-turbo
2024-10-12T02:15:44Z INFO Retrying request after backoff...
2024-10-12T02:16:01Z FATAL job aborted after 5 retries
Simultaneously, the cluster metrics showed CPU and memory spikes, and the kubectl get jobs output listed multiple active jobs for the same CronJob:
NAME COMPLETIONS DURATION AGE
gpt-batch-20241012-0200-1 0/1 12m 12m
gpt-batch-20241012-0200-2 0/1 12m 12m
These overlapping executions caused:
- API rate‑limit violations on the OpenAI endpoint.
- Resource contention on the GPU‑enabled node pool.
- Increased cost due to duplicate work.
Root Cause
The CronJob was defined with a schedule: "0 2 * * *" (run at 02:00 UTC) and a concurrencyPolicy: Allow. The job’s runtime varied between 8 minutes and 20 minutes depending on the input size. When a previous run exceeded its expected duration, the next scheduled start created a second pod while the first was still active. Because concurrencyPolicy was set to Allow, Kubernetes launched the second job instead of skipping or replacing it.
Additional factors:
- The CronJob’s
startingDeadlineSecondswas omitted, so missed starts were not reported. - Pod termination grace period was 30 seconds, insufficient for the batch process to flush pending OpenAI requests.
- The OpenAI client library used exponential backoff, which amplified the rate‑limit errors when multiple pods hit the same endpoint simultaneously.
Debug
Step‑by‑step investigation:
- Inspect CronJob definition
kubectl get cronjob gpt-batch -o yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: gpt-batch
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Allow
jobTemplate:
spec:
template:
spec:
containers:
- name: gpt-runner
image: myrepo/gpt-batch:1.4
envFrom:
- secretRef:
name: openai-credentials
restartPolicy: OnFailure
kubectl get jobs -l job-name=gpt-batch --field-selector status.active=1
NAME ACTIVE START TIME
gpt-batch-20241012-0200-1 1 2024-10-12T02:00:05Z
gpt-batch-20241012-0200-2 1 2024-10-12T02:00:07Z
kubectl logs -l job-name=gpt-batch-20241012-0200-1
2024-10-12T02:15:43Z ERROR rate limit exceeded for model gpt-3.5-turbo
...
OpenAI dashboard showed requests_per_minute spiking from 300 rpm (normal) to 1,200 rpm during the overlap window, exceeding the account’s limit of 1,000 rpm.
kubectl get jobs -l job-name=gpt-batch -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.completionTime}{" "}{.status.startTime}{"\n"}{end}'
gpt-batch-20241012-0200-1 2024-10-12T02:18:30Z 2024-10-12T02:00:05Z
gpt-batch-20241012-0200-2 2024-10-12T02:22:45Z 2024-10-12T02:00:07Z
Both jobs ran >18 minutes, confirming overlap.
Solution
Three complementary changes were applied:
1. Enforce non‑overlapping execution
Set concurrencyPolicy: Forbid so that a new schedule is skipped if a previous run is still active.
2. Add a safety window with startingDeadlineSeconds
This fails the schedule if the controller cannot start the job within the defined deadline, surfacing missed runs.
3. Graceful shutdown handling
Increase terminationGracePeriodSeconds to allow the batch process to finish pending OpenAI calls.
Updated CronJob manifest (before / after):
# BEFORE (problematic)
apiVersion: batch/v1
kind: CronJob
metadata:
name: gpt-batch
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Allow
jobTemplate:
spec:
template:
spec:
containers:
- name: gpt-runner
image: myrepo/gpt-batch:1.4
restartPolicy: OnFailure
# AFTER (fixed)
apiVersion: batch/v1
kind: CronJob
metadata:
name: gpt-batch
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid # Prevent overlap
startingDeadlineSeconds: 300 # 5‑minute start window
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
terminationGracePeriodSeconds: 300 # 5 minutes for clean shutdown
containers:
- name: gpt-runner
image: myrepo/gpt-batch:1.4
envFrom:
- secretRef:
name: openai-credentials
restartPolicy: OnFailure
4. Rate‑limit aware client
Wrap the OpenAI SDK call with a token bucket limiter (e.g., golang.org/x/time/rate) to cap requests at 900 rpm, staying below the account limit.
package main
import (
"context"
"time"
"golang.org/x/time/rate"
openai "github.com/sashabaranov/go-openai"
)
var limiter = rate.NewLimiter(rate.Every(time.Minute/900), 1) // 900 rpm
func callGPT(prompt string) (string, error) {
ctx := context.Background()
if err := limiter.Wait(ctx); err != nil {
return "", err
}
client := openai.NewClient(os.Getenv("OPENAI_API_KEY"))
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: "gpt-3.5-turbo",
Messages: []openai.ChatCompletionMessage{
{Role: "user", Content: prompt},
},
})
if err != nil {
return "", err
}
return resp.Choices[0].Message.Content, nil
}
Verify
After applying the manifest and redeploying, the following checks confirm the fix:
- Observe that only one job is active at any time.
kubectl get jobs -l job-name=gpt-batch -o wide
NAME COMPLETIONS DURATION AGE
gpt-batch-20241013-0200-1 1/1 14m 14m
2024-10-13T02:14:55Z INFO shutting down, flushing pending requests...
2024-10-13T02:15:00Z INFO shutdown complete
kubectl create job --from=cronjob/gpt-batch test-overlap
Error from server (BadRequest): job "test-overlap" is invalid: spec.template.spec.containers[0].name: Invalid value: "gpt-runner": already running
Prevent
- Monitoring: Add a Prometheus alert on
kube_job_status_active{job_name="gpt-batch"}> 1. - Alerting: Trigger PagerDuty if OpenAI
rate_limit_exceedederrors appear more than 3 times in a 5‑minute window. - CI/CD guardrails: Lint CronJob manifests for
concurrencyPolicy: ForbidandstartingDeadlineSecondsusingkube-linterorOPA Gatekeeper. - Capacity planning: Size the GPU node pool to handle the worst‑case batch duration plus a 20 % buffer.
- Documentation: Record expected job runtime in the runbook; update the schedule if the pipeline’s data volume grows.
FAQ
- Why does
concurrencyPolicy: Allowcause rate‑limit errors?
Because it permits multiple pods to invoke the OpenAI endpoint simultaneously, multiplying request throughput and easily exceeding the account’s RPM quota. - Can I keep
Allowand still avoid overlap?
Only by adding external coordination (e.g., a ConfigMap lock) or by configuring the job to exit early if a lock exists. UsingForbidis the native, simpler solution. - What is a reasonable value for
startingDeadlineSeconds?
It should be longer than the expected scheduling latency but shorter than the maximum acceptable delay for the batch. Five minutes (300 s) works for most nightly jobs. - How do I tune the token‑bucket limiter for different OpenAI plans?
Set the limiter rate to(plan_limit_rpm * safety_factor). A safety factor of 0.9 provides a margin for occasional spikes. - My job still sometimes exceeds the schedule window. Should I increase the interval?
If the workload regularly exceeds the allocated window, either increase the interval (e.g., run at 01:00 UTC) or split the batch into smaller parallel jobs with independent rate‑limit budgets.
Related Topic Hub: LLM Systems Troubleshooting Hub