2026-10-31 · 6 min read
Caption API Rate Limits and Batching: A Queue That Never Trips Over Itself
One video through the caption.sh API is a single request. A backlog of fifty is a different shape of problem: submit too many at once and the API protects itself with per-key rate limits, answering the excess with HTTP 429. None of that is a reason to caption fewer videos. It is a reason to put a small queue between your backlog and the API.
The pattern that works is boring on purpose: submit a few jobs, poll them to completion, submit more. When a submit comes back with a 429, wait briefly and try again. Jobs that are already submitted are unaffected by the limit; it only gates new submissions per key.
A batch runner with backoff
import time, requests
KEY = "YOUR_CAPTION_API_KEY"
HEADERS = {"Authorization": f"Bearer {KEY}"}
API = "https://api.caption.sh/v1"
def submit(url: str):
for attempt in range(6):
r = requests.post(
f"{API}/videos",
headers=HEADERS,
json={"source_url": url, "options": {"font": "inter"}},
timeout=30,
)
if r.status_code == 429:
time.sleep(10 * (attempt + 1))
continue
job_id = r.json().get("job_id")
if job_id:
return job_id
raise RuntimeError(f"submit failed: {r.status_code} {r.text}")
raise RuntimeError("submit kept hitting 429")
def wait(job_id: str) -> str:
deadline = time.time() + 20 * 60
while time.time() < deadline:
job = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
if job.get("status") == "done":
r = requests.get(f"{API}/jobs/{job_id}/result", headers=HEADERS, allow_redirects=False, timeout=30)
return r.headers["Location"]
if job.get("status") == "error":
raise RuntimeError(f"job failed: {job}")
time.sleep(5)
raise RuntimeError("timed out")
for url in open("backlog.txt"):
url = url.strip()
if not url:
continue
job = submit(url)
print(url, "->", wait(job))Three details keep this honest. The submit loop retries only on 429 and gives up after a bounded number of attempts, so a real failure surfaces instead of looping forever. The poll loop has a deadline and treats a failed status as an error, so a bad input video cannot hold the queue hostage. And results are downloaded right away, since they are stored for 24 hours and then deleted.
Practical notes for large batches
- Keep the concurrency low and steady. A handful of in-flight jobs per key keeps the pipeline moving without tripping the limiter.
- Record job IDs as you go. If the runner dies, the jobs are still processing and results are still stored for 24 hours before deletion.
- Cost stays predictable at $0.10 per minute, so a backlog's price is known before the first request goes out.
Rate limits feel like friction until you realize they are the only thing between a tidy queue and a hammered API. Twenty lines of backoff later, batch captioning runs overnight and the captioned files are in the folder by morning. Get a key at caption.sh and point it at your backlog.