Kubernetes CronJob concurrency policy for paid API jobs
Allow, Forbid, or Replace? A CronJob's concurrencyPolicy governs its Jobs, not the API work they start. For paid API calls, add an idempotency key.

A Kubernetes CronJob's concurrencyPolicy decides what happens when a run comes due while the previous Job is still running: Allow, the default, runs both; Forbid skips the new run; Replace replaces the running Job with a new one. When the Job calls a paid API that runs for minutes, such as AI video generation, use Forbid and make the call idempotent, because stopping a pod doesn't stop work the API has already accepted.
Kubernetes facts come from its CronJob, Jobs, and kubectl create job pages; Sume facts come from Create a run, Runs and results, and Errors and spend. All were read on 2026-09-27. Sume has no Kubernetes operator: the pod makes one plain HTTPS call.
What does each concurrencyPolicy value do to a paid API call?
The policy only applies to Jobs created by the same CronJob. Kubernetes also warns that a CronJob creates a Job only approximately once per scheduled time: in some circumstances it creates two, or none, so the Jobs you define should be idempotent.
| Value | What Kubernetes does | For a paid API call |
|---|---|---|
Allow (default) | Runs concurrent Jobs | Two pods can submit at the same time; for the same slot, a shared idempotency key stops a second charge |
Forbid | Skips the new run while the previous one is unfinished; the skip counts as missed | The safe choice, as long as the pod finishes quickly |
Replace | Replaces the running Job with a new Job run | The run the old pod submitted keeps running and billing; the new pod may start another |
Does Replace or activeDeadlineSeconds stop the API spend?
No. Both act on Jobs and their pods. Once a Job reaches activeDeadlineSeconds, Kubernetes terminates its running pods and marks it failed with reason DeadlineExceeded; Replace swaps the Job. Neither reaches the API. On Sume, abandoning a poll loop does not stop the run or its spend, and a timeout does not cancel it. A Format run ends on its own at the latest 90 minutes after created_at, when Sume force-finalizes it as failed.
- To stop spend, cancel the run itself:
POST /v1/format-runs/{run_id}/cancelwithformats:write. Generation the run completed before the cancel is still billed, and a canceled run sends no webhook. - Generation jobs, such as a
POST /v1/videosjob, cancel only before generation starts. After that, cancel answers409 job_generation_already_startedand the job runs to completion.
How do I stop a retried or duplicate Job from paying twice?
Build the Idempotency-Key and the body from the scheduled slot, such as the UTC date for a daily run, and nothing else. Every attempt at that slot then sends the identical request: a pod retry (a Job's backoffLimit defaults to 6, with back-off delays of 10 s, 20 s, 40 s and so on, capped at six minutes), a second Job for the same time, or a manual rerun. Keys are scoped to one Format and are up to 255 characters.
- Same key, same body:
200with the original receipt andidempotency_hit: true. No second run, no second charge. - Same key, different body:
409 idempotency_conflict, and nothing runs. Never put a timestamp or a random value in the body. - Same key, two requests at the same moment: one wins; the other gets
409 idempotency_key_in_use, which is retryable after about a second. - A create that failed (
402,503, …) released the key, so the pod's retry can still start the slot's run.
#!/bin/sh
# submit-nightly.sh: one Format run per UTC day, then exit
set -eu
SLOT=$(date -u +%Y-%m-%d) # UTC, like the CronJob's timeZone
curl --fail-with-body -sS -X POST \
"https://api.sume.com/v1/formats/acme/daily-recap/runs" \
-H "Authorization: Bearer $SUME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: daily-recap-$SLOT" \
-d "{
\"input\": { \"date\": \"$SLOT\" },
\"on_active_run\": \"skip\",
\"communication\": { \"webhook_url\": \"https://example.com/hooks/sume\" }
}"Should the pod wait for the video to finish?
No: submit and exit. A pod that waits keeps its Job running for the whole render, so under Forbid a render that outlasts the interval skips the next slot, and any deadline kills only the waiting. With communication.webhook_url, Sume POSTs the run's receipt once, when it completes or fails, to a receiver you run as an ordinary Deployment; the receipt's trigger.idempotency_key tells it which slot finished. --fail-with-body makes curl return an error on a status of 400 or more while still printing Sume's error, so a refused submit fails the pod.
A pod that exits in seconds also means Forbid no longer sees the render, so guard overlap on Sume's side too. A Format run's on_active_run defaults to allow; skip records a skipped run and starts nothing, and reject answers 409 format_run_in_progress. Prevent overlapping AI agent runs compares the two.
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-recap
spec:
schedule: "0 2 * * *"
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 3
activeDeadlineSeconds: 300 # bounds the submit, not the video
template:
spec:
restartPolicy: Never
containers:
- name: submit
image: registry.example.com/submit-nightly:1 # sh, date, curl
command: ["/bin/sh", "/app/submit-nightly.sh"]
env:
- name: SUME_API_KEY
valueFrom:
secretKeyRef:
name: sume-api
key: api-keyHow do I trigger a CronJob manually, and in which time zone?
kubectl create job daily-recap-manual --from=cronjob/daily-recap creates a Job from the CronJob's template; CronJob is the only resource --from supports. Because the key comes from the slot, a manual run on a day that already ran replays that day's run and starts nothing. To force a fresh one, add a version you bump to the key, which is how Sume's docs say to request a deliberate re-run.
Set .spec.timeZone, stable since Kubernetes v1.27, to a zone name such as Etc/UTC. Without it, the kube-controller-manager reads the schedule in its own local time zone, and the pod's slot and the schedule can disagree around midnight.
Sources
Related posts
More in Integrations
- LangChain video generation tool that runs a Sume Format
A LangChain @tool can start a Sume Format run, cap its spend, key it for safe retries, and return a run id the agent checks until the video is ready.
- LlamaIndex image generation tool with the Sume Image API
Wrap POST /v1/images in a LlamaIndex FunctionTool: send sume/auto and a prompt, return URLs on a 200, and hand back the job id on a 202.
- MCP server for LM Studio: add Sume's hosted MCP
LM Studio 0.3.17 and later can use remote MCP servers. Add Sume's hosted MCP to mcp.json with an API-key header, then confirm each paid call.
- Make.com AI video scenario with Sume: HTTP and a webhook
Split a Make.com AI video scenario in two: Make a request starts a Sume run; a custom webhook takes the signed result and checks it with sha256().
Written by Sume