← Back to blog

Deploy a Serverless YouTube Transcript Function

Zied · 8/9/2026 · 8 min read

Deploy a Serverless YouTube Transcript Function

Transcript workloads are bursty by nature: a user clicks a button, one API call fires, and the function sits idle until the next request. That pattern maps perfectly onto serverless, where you pay only when code runs and scale is automatic.

This guide walks through deploying a lightweight proxy function on Cloudflare Workers or AWS Lambda that fetches YouTube transcript JSON from the YouTube Transcriber API, keeps the Bearer token out of client code, and adds response caching to avoid paying for the same transcript twice.


Why Serverless Fits Transcript Workloads

A transcript fetch is stateless: you send a video ID, you get JSON back. There is no session, no database connection to warm, and no shared state across requests. That makes it a textbook candidate for a function-as-a-service deployment.

Three practical reasons this works well in production:

Bursty traffic without idle cost. A content tool might fetch zero transcripts at 3 AM and hundreds during a product launch. Serverless scales to zero between bursts, so you are not paying for an always-on server to watch for requests.

No infrastructure to maintain. You write a handler, deploy it, and the platform manages runtimes, load balancers, and OS patches. For a proxy function this thin, an EC2 instance or container would be engineering overhead with no benefit.

Pay-per-call lines up with upstream billing. YouTube Transcriber charges only for successful calls. A serverless function that charges only when it executes mirrors that model cleanly. If the upstream call fails, you spend neither a credit nor a compute millisecond on wasted work. The pay-per-successful-call pricing model explains why this alignment matters at scale.


The Transcript Endpoint

Before writing any function code, understand what you are proxying. The YouTube Transcriber transcript endpoint is:

GET https://getyoutubetranscriber.com/api/v2/transcript

Required parameter:

| Parameter | Type | Description | |----------|------|-------------------------------| | video_url | string | YouTube video ID or full YouTube URL |

Useful optional parameters:

| Parameter | Default | Description | |----------------|-------|-------------------------------------| | lang | original | Caption language code, e.g. fr, pt-BR | | format | json | json or text | | include_timestamp | true | Include start and duration per segment | | send_metadata | true | Include title, channel name, thumbnail URL |

A successful response returns transcript JSON like this:

{
  "video_id": "dQw4w9WgXcQ",
  "language": "en",
  "transcript": [
    { "text": "We're no strangers to love", "start": 18640, "duration": 3240 },
    { "text": "You know the rules and so do I", "start": 21880, "duration": 2760 }
  ],
  "metadata": {
    "title": "Rick Astley - Never Gonna Give You Up",
    "author_name": "Rick Astley",
    "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg"
  }
}

Note that start and duration are in milliseconds, not seconds. Factor that in when you parse timestamps downstream. The guide on parsing transcript JSON to clean text covers common parsing pitfalls in detail.


Deploy a Cloudflare Worker Proxy

Cloudflare Workers run on V8 isolates rather than containers, which eliminates cold starts almost entirely. The typical startup latency is under 1 millisecond, compared to hundreds of milliseconds for a container-based Lambda cold start.

Step 1: Store the Bearer token as a secret

Never put the token in wrangler.toml or committed source code. Use the Wrangler CLI instead:

wrangler secret put YT_TRANSCRIPT_API_KEY

The CLI prompts for the value and encrypts it at rest. Your Worker accesses it through env.YT_TRANSCRIPT_API_KEY at runtime without the value ever appearing in your repository.

Step 2: Write the Worker handler

// src/index.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const videoId = url.searchParams.get("video_id");
    const lang = url.searchParams.get("lang") || "";

    if (!videoId) {
      return new Response(JSON.stringify({ error: "Missing video_id" }), {
        status: 400,
        headers: { "Content-Type": "application/json" },
      });
    }

    // Check KV cache first
    const cacheKey = `transcript:${videoId}:${lang}`;
    const cached = await env.TRANSCRIPT_CACHE.get(cacheKey, "json");
    if (cached) {
      return new Response(JSON.stringify(cached), {
        headers: {
          "Content-Type": "application/json",
          "X-Cache": "HIT",
        },
      });
    }

    // Fetch from YouTube Transcriber
    const apiUrl = new URL("https://getyoutubetranscriber.com/api/v2/transcript");
    apiUrl.searchParams.set("video_url", videoId);
    if (lang) apiUrl.searchParams.set("lang", lang);

    const apiResponse = await fetch(apiUrl.toString(), {
      headers: {
        Authorization: `Bearer ${env.YT_TRANSCRIPT_API_KEY}`,
        "Content-Type": "application/json",
      },
    });

    if (!apiResponse.ok) {
      const err = await apiResponse.json().catch(() => ({}));
      return new Response(JSON.stringify(err), {
        status: apiResponse.status,
        headers: { "Content-Type": "application/json" },
      });
    }

    const data = await apiResponse.json();

    // Cache for 6 hours; transcripts are stable after publication
    await env.TRANSCRIPT_CACHE.put(cacheKey, JSON.stringify(data), {
      expirationTtl: 21600,
    });

    return new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "X-Cache": "MISS",
      },
    });
  },
};

Step 3: Configure wrangler.toml

name = "yt-transcript-proxy"
main = "src/index.js"
compatibility_date = "2025-01-01"

[[kv_namespaces]]
binding = "TRANSCRIPT_CACHE"
id = "YOUR_KV_NAMESPACE_ID"

Create the KV namespace with:

wrangler kv:namespace create "TRANSCRIPT_CACHE"

Then deploy:

wrangler deploy

Deploy an AWS Lambda Proxy

If your stack already lives in AWS, a Lambda function behind API Gateway or a Function URL achieves the same result.

Store the token as an environment secret

Avoid plaintext environment variables for sensitive values. Use AWS Secrets Manager and retrieve the secret at cold-start time:

import boto3
import json
import urllib.request
import urllib.parse
import os

_secret = None

def get_api_key():
    global _secret
    if _secret:
        return _secret
    client = boto3.client("secretsmanager", region_name=os.environ["AWS_REGION"])
    response = client.get_secret_value(SecretId="yt-transcript-api-key")
    _secret = json.loads(response["SecretString"])["api_key"]
    return _secret

def lambda_handler(event, context):
    params = event.get("queryStringParameters") or {}
    video_id = params.get("video_id")
    lang = params.get("lang", "")

    if not video_id:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Missing video_id"}),
        }

    query = {"video_url": video_id}
    if lang:
        query["lang"] = lang

    api_url = (
        "https://getyoutubetranscriber.com/api/v2/transcript?"
        + urllib.parse.urlencode(query)
    )

    req = urllib.request.Request(
        api_url,
        headers={"Authorization": f"Bearer {get_api_key()}"},
    )

    try:
        with urllib.request.urlopen(req, timeout=25) as resp:
            body = resp.read()
            return {
                "statusCode": 200,
                "headers": {"Content-Type": "application/json"},
                "body": body.decode("utf-8"),
            }
    except urllib.error.HTTPError as e:
        return {
            "statusCode": e.code,
            "body": e.read().decode("utf-8"),
        }

Caching in Lambda is simpler with ElastiCache or DynamoDB. For lower-volume use cases, an in-process dictionary keyed on (video_id, lang) persists across warm invocations and costs nothing to operate.


Secrets: The Non-Negotiable Rule

Both platforms have the same fundamental constraint: the Bearer token must never reach client code. If a browser, mobile app, or third-party service can read it, every user of that code can make authenticated API calls on your account.

The pattern is consistent across platforms:

  • Cloudflare Workers: wrangler secret put stores the value encrypted; read via env.SECRET_NAME
  • AWS Lambda: Secrets Manager or Parameter Store; retrieve once at cold start, cache in module scope
  • Vercel / Netlify Functions: project-level environment variables marked as secret in the dashboard

The function itself is the trust boundary. Clients call your function. Your function calls the upstream API with the token. The token never travels further than the server side.


Add Response Caching to Cut Costs

A transcript for a given video in a given language does not change after publication. Fetching it repeatedly wastes credits and adds latency. Cache the response keyed on video_id + lang.

Cloudflare KV works well for Workers: globally replicated, low-latency reads, and TTL-based expiration. The code above shows a 6-hour TTL, which is conservative. For videos older than a week, a TTL of 24 hours or longer is reasonable.

Lambda with DynamoDB is a common pattern for AWS deployments. Store the JSON under a composite key and set a TTL attribute. A Lambda cold start fetches from DynamoDB before calling the upstream API.

In-memory caching works for low-volume deployments where a single warm instance handles most traffic. It is not durable across restarts or multiple instances, but it is zero-cost and zero-configuration. Store fetched responses in a module-level dictionary with an expiry timestamp check.

Whatever you choose, the cache key should always include the language code. A request for video_id=abc&lang=fr and video_id=abc&lang=es are different responses.


Gotchas: Cold Starts and Timeouts

Cold starts

Cloudflare Workers have near-zero cold starts because they use V8 isolates rather than container snapshots. AWS Lambda cold starts for Node.js and Python are typically in the 200-400 ms range with lean dependencies. JVM runtimes can push cold starts into the seconds. If you use Lambda, keep your deployment package small and avoid heavy SDKs in the import chain.

The cached Secrets Manager call pattern shown above also helps: retrieving the secret on the first invocation adds latency only on cold starts, not on warm ones.

Timeout defaults are too low for long transcripts

AWS Lambda's default timeout is 3 seconds. A dense hour-long video with auto-generated captions can take longer than that to fetch and process, especially on the first call when the upstream API is doing its own proxy and retry work.

Set your Lambda timeout to at least 15-30 seconds. On Cloudflare Workers paid plans, the default CPU time limit is 30 seconds, with a maximum of 5 minutes. For most transcripts that is more than sufficient, but be aware that CPU time (processing) and wall clock time (waiting on network) are tracked separately on Workers. The fetch to the YouTube Transcriber API counts against wall clock time, not CPU time, so long network waits will not burn through your CPU budget.

If you are processing transcripts for very long videos in bulk, the retry strategies guide covers timeout handling and backoff patterns for production pipelines.

Proxy your function, not just the API

One common mistake is building the proxy and then calling the upstream API directly from the client too, as a fallback. That bypasses your caching layer and exposes the token. Enforce the proxy as the single path: the client calls your function, period.


What to Build Next

With a working serverless proxy in place, you have a reusable building block. Drop it behind a frontend to power a video summarizer. Feed transcript JSON into an embedding pipeline for semantic search. Trigger it on a schedule to monitor new channel uploads for keywords.

The YouTube Transcriber API docs cover the full transcript endpoint reference, including bulk transcript requests via POST, language enumeration, and error codes. The free tier starts with 100 credits and no credit card required, which is enough to validate the integration end-to-end before you scale.