# YouTube transcript script works locally, fails deployed

> Why a YouTube transcript script gets blocked once it runs on a server, and how to handle every CleanScript error in production: retry, skip or fix.

Updated 2026-10-06. HTML: /blog/youtube-transcripts-in-production

A transcript script that works on your laptop and fails on a server is usually being blocked. YouTube blocks most IP addresses that belong to cloud providers, so `youtube-transcript-api` raises `RequestBlocked` or `IpBlocked` once you deploy. The library's [README](https://github.com/jdepoix/youtube-transcript-api#working-around-ip-bans-requestblocked-or-ipblocked-exception) says so and recommends rotating residential proxies.

The official YouTube Data API won't help with other people's videos: downloading a caption track [requires permission to edit the video](https://developers.google.com/youtube/v3/docs/captions/download).

So you run proxies yourself or call a hosted API; [youtube-transcript-api alternatives](/blog/youtube-transcript-api-alternatives) compares them. With CleanScript, your server never calls YouTube. What's left is handling the answers.

## Three kinds of failure

| Kind | Codes | What to do |
|---|---|---|
| Try again later | `rate_limited` (429), `source_unavailable` (503), `processing_failed` (503), `timeout` (504) | Wait for `Retry-After`, then retry. |
| Nothing to get | `video_unavailable` (404), `no_captions`, `language_unavailable`, `video_too_long` (422) | Retrying won't help. Skip it and log why. |
| Fix the request | `invalid_url`, `unsupported_platform`, `invalid_request` (400), `unauthorized` (401), `insufficient_credits` (402) | Fix the input, the key or the balance. |

No failed request uses credits. Every error has the same shape:

```json
{
  "error": {
    "code": "video_unavailable",
    "message": "This video is private.",
    "param": "url",
    "details": { "reason": "private" }
  },
  "request_id": "5f0c7d1e-0b7a-4a39-9c1e-3c1f2b8f6a10"
}
```

Branch on `code`, never on `message`. Keep `request_id` in your logs; if something looks wrong, [send it to us](mailto:mohamed@messaad.dev) and we can find the request. The full list is in [Credits & limits](/docs/credits).

## The SDKs retry for you

The Python and TypeScript clients retry `429`, `503` and network errors twice with backoff, honouring `Retry-After`. Each transcript and extract call carries an idempotency key, so a retry is never charged twice. When retries run out, you get a typed error:

```python
from cleanscript_ai import CleanScript, CleanScriptError, InsufficientCreditsError, VideoUnavailableError

client = CleanScript(max_retries=4)

def get_text(url):
    try:
        return client.transcript(url).text
    except VideoUnavailableError as error:
        print("skip:", url, error.details)  # reason: private, removed, age_restricted, live or unavailable
        return None
    except InsufficientCreditsError as error:
        raise SystemExit(f"Out of credits: {error.details['top_up_url']}")
    except CleanScriptError as error:
        if error.code in {"no_captions", "language_unavailable", "video_too_long"}:
            print("skip:", url, error.code)
            return None
        raise
```

The other subclasses are `AuthenticationError`, `RateLimitError` and `ConnectionFailedError`; everything else is a `CleanScriptError` with a `code`.

## Production checklist

- Keep the key in a secret store and read it from `CLEANSCRIPT_API_KEY`.
- Run a few requests at a time. Your limit per minute is in `GET /v1/account`, which is free.
- Log `request_id` and the `X-Credits-Remaining` header with each job.
- Retry only the first kind of failure. Record the second and move on.
- Stop the queue on `insufficient_credits` instead of failing every item.
- The same request within 6 hours is free, so re-running a half-finished batch costs only what's new.

No-code tools follow the same rules: see [transcripts in n8n, Make and Zapier](/blog/transcripts-in-automations-http).
