← Back to blog

Caching YouTube Transcripts to Cut API Costs

David Boulen · 7/12/2026 · 9 min read

Caching YouTube Transcripts to Cut API Costs

Caching YouTube Transcripts to Cut API Costs

Every time your application fetches a transcript it has already fetched before, you spend a credit you did not need to spend. For apps that serve the same videos repeatedly - a summarizer, a RAG pipeline, a content repurposing tool - those redundant calls add up fast. A well-placed cache layer eliminates them.

This guide covers how to design a cache for YouTube transcript API responses: picking the right cache key, choosing a TTL strategy, implementing it in Python, and knowing when to invalidate. At the end you will have a reusable pattern that can realistically cut your credit usage by 70-90% in production.

Server infrastructure with illuminated racks representing cached API responses

Why Transcripts Are Highly Cacheable

Not every API response is worth caching. Caching makes sense when the data is stable and the same request recurs frequently. YouTube transcripts satisfy both conditions better than almost any other content type.

Once a video is published and its auto-captions have been processed, the transcript text does not change unless the creator manually edits the captions in YouTube Studio. For established videos, that event is rare. In practice:

  • Auto-captions are generated within minutes to hours of upload and may see minor revisions in the first 24-48 hours.
  • After the first week, caption tracks are effectively frozen until someone explicitly touches them.
  • Most transcript fetches in production apps target popular, older videos rather than content published in the last hour.

This means the data you get back from the API today is almost certainly identical to what you will get tomorrow and next week. That stability is the foundation of an aggressive caching strategy.

The other condition, repeated requests, is typically met by any app with a user base. Users watch the same viral videos, educators assign the same lectures, and research pipelines revisit the same channels repeatedly. If ten users each trigger a transcript fetch for the same video in the same hour, nine of those calls are pure waste without a cache.

Choosing a Cache Key and TTL Strategy

Cache key design

The cache key must uniquely identify a specific transcript response. A video ID alone is not enough: the same video can have multiple caption tracks in different languages, and a cache miss on a language boundary means you serve the wrong data.

Use a composite key that combines the video ID and the language code:

transcript:{video_id}:{language}

For example: transcript:dQw4w9WgXcQ:en

If your application requests transcripts in specific formats, include that in the key too. But keep the key as short as possible - everything you add increases the surface area for misses.

TTL strategy by video age

A single TTL value for all transcripts is a blunt instrument. New videos are still being processed; old videos are stable. The right approach is tiered:

| Video age | Recommended TTL | Rationale | | - -- - -- - -| - -- - -- - -- - --| - -- - -- - -| | 0-24 hours | 1 hour | Auto-captions are still being generated and corrected | | 1-7 days | 6 hours | Caption refinements slow down but can still occur | | 7-30 days | 7 days | Effectively stable; rare manual edits | | 30+ days | 30 days | Treat as permanent for practical purposes |

You can determine a video's age from the published timestamp included in the transcript API response. Parse it once, pick the TTL, and store alongside the cached transcript.

If you do not have easy access to the publish date at request time, a conservative flat TTL of 24 hours covers most cases without a measurable freshness risk. The Understanding YouTube Transcript Rate Limits guide covers how rate pressure compounds when you skip caching and repeatedly re-fetch the same content.

Implementing a Cache Layer in Python

The right cache backend depends on your deployment. For a single-server or serverless setup, diskcache is the practical choice: it is backed by SQLite, survives process restarts, handles large text payloads efficiently, and requires no extra infrastructure. For distributed deployments with multiple workers, swap in Redis using the same interface pattern.

Installing dependencies

pip install diskcache requests

Basic cache wrapper

import hashlib
import time
import requests
from diskcache import Cache

CACHE_DIR = "/tmp/yt_transcript_cache"
API_BASE = "https://getyoutubetranscriber.com/api/v2"
API_KEY = "YOUR_BEARER_TOKEN"

cache = Cache(CACHE_DIR, size_limit=10 * 10**9)  # 10 GB ceiling

def get_ttl_for_age(published_at_iso: str | None) -> int:
    """Return TTL in seconds based on video publish date."""
    if published_at_iso is None:
        return 86400  # Default: 24 hours when age is unknown

    try:
        from datetime import datetime, timezone
        published = datetime.fromisoformat(published_at_iso.replace("Z", "+00:00"))
        age_days = (datetime.now(timezone.utc) - published).days
    except (ValueError, TypeError):
        return 86400

    if age_days < 1:
        return 3600        # 1 hour
    elif age_days < 7:
        return 21600       # 6 hours
    elif age_days < 30:
        return 604800      # 7 days
    else:
        return 2592000     # 30 days


def fetch_transcript(video_id: str, language: str = "en") -> dict:
    """Fetch a transcript, returning the cached version if available."""
    cache_key = f"transcript:{video_id}:{language}"

    cached = cache.get(cache_key)
    if cached is not None:
        cached["_cache_hit"] = True
        return cached

    response = requests.get(
        f"{API_BASE}/transcript",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params={"videoId": video_id, "language": language},
        timeout=15,
    )
    response.raise_for_status()
    data = response.json()

    published_at = data.get("publishedAt")
    ttl = get_ttl_for_age(published_at)

    cache.set(cache_key, data, expire=ttl)
    data["_cache_hit"] = False
    return data

The _cache_hit flag is a low-cost instrumentation hook. You will use it in the measurement section below.

Bulk transcript requests

YouTube Transcriber's /api/v2/transcripts-bulk endpoint accepts up to 50 video IDs per call. You can cut API usage further by checking the cache for each ID first and only sending uncached IDs to the bulk endpoint:

def fetch_transcripts_bulk(video_ids: list[str], language: str = "en") -> dict[str, dict]:
    """Fetch multiple transcripts, hitting the cache first."""
    results = {}
    uncached_ids = []

    for vid in video_ids:
        cache_key = f"transcript:{vid}:{language}"
        cached = cache.get(cache_key)
        if cached is not None:
            results[vid] = {**cached, "_cache_hit": True}
        else:
            uncached_ids.append(vid)

    if not uncached_ids:
        return results

    response = requests.post(
        f"{API_BASE}/transcripts-bulk",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"videoIds": uncached_ids, "language": language},
        timeout=30,
    )
    response.raise_for_status()

    for item in response.json().get("transcripts", []):
        vid = item["videoId"]
        published_at = item.get("publishedAt")
        ttl = get_ttl_for_age(published_at)
        cache.set(f"transcript:{vid}:{language}", item, expire=ttl)
        results[vid] = {**item, "_cache_hit": False}

    return results

If you are building a pipeline that pulls large transcript datasets, the Download YouTube Transcripts in Bulk Without Getting Blocked guide covers complementary strategies for managing throughput at scale.

Python code visualized as flowing data streams representing caching implementation

Invalidating When Videos Update Captions

Invalidation is the hard part of caching. For YouTube transcripts you have two scenarios that require it.

Manual invalidation

The simplest pattern: expose a function that clears a cache entry by key. Call it whenever you know a transcript has changed.

def invalidate_transcript(video_id: str, language: str = "en") -> bool:
    """Remove a cached transcript entry. Returns True if the key existed."""
    cache_key = f"transcript:{video_id}:{language}"
    return cache.delete(cache_key)


def invalidate_all_languages(video_id: str) -> int:
    """Remove all cached language variants for a video."""
    prefix = f"transcript:{video_id}:"
    deleted = 0
    for key in list(cache.iterkeys()):
        if str(key).startswith(prefix):
            cache.delete(key)
            deleted += 1
    return deleted

Conditional refresh for monitored channels

If you track a channel and need to detect caption changes, the cleanest approach is to store a hash of the transcript content alongside the cached entry. On each check, fetch the transcript, hash it, compare to the stored hash, and only write a new cache entry if the hash differs. This lets you run freshness checks without paying for a full API call every time, since a conditional check can be done after reading the locally cached hash.

def is_transcript_stale(video_id: str, language: str = "en") -> bool:
    """Check if a cached transcript may be outdated by re-fetching and comparing."""
    cache_key = f"transcript:{video_id}:{language}"
    cached = cache.get(cache_key)
    if cached is None:
        return True  # Not cached at all

    # Fetch fresh copy (costs one credit)
    response = requests.get(
        f"{API_BASE}/transcript",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params={"videoId": video_id, "language": language},
        timeout=15,
    )
    response.raise_for_status()
    fresh = response.json()

    cached_hash = hashlib.sha256(str(cached.get("transcript", "")).encode()).hexdigest()
    fresh_hash = hashlib.sha256(str(fresh.get("transcript", "")).encode()).hexdigest()

    if cached_hash != fresh_hash:
        ttl = get_ttl_for_age(fresh.get("publishedAt"))
        cache.set(cache_key, fresh, expire=ttl)
        return True

    return False

Use this pattern sparingly. It costs one API credit per check, so only run it for channels where caption accuracy is critical, such as accessibility workflows or legal transcription. For most content pipelines the TTL-based approach is sufficient.

Measuring Credit Savings

You cannot tune what you do not measure. The _cache_hit flag added earlier gives you the raw signal. Wrap it in a simple counter:

from collections import defaultdict

_stats = defaultdict(int)

def fetch_transcript_tracked(video_id: str, language: str = "en") -> dict:
    result = fetch_transcript(video_id, language)
    if result.get("_cache_hit"):
        _stats["cache_hits"] += 1
    else:
        _stats["api_calls"] += 1
    return result


def print_savings_report(credit_cost_per_call: float = 1.0) -> None:
    hits = _stats["cache_hits"]
    calls = _stats["api_calls"]
    total = hits + calls
    hit_rate = (hits / total * 100) if total else 0
    saved_credits = hits * credit_cost_per_call

    print(f"Total requests : {total}")
    print(f"Cache hits     : {hits}  ({hit_rate:.1f}%)")
    print(f"API calls made : {calls}")
    print(f"Credits saved  : {saved_credits:.0f}")

Run this report at the end of each batch job or on a daily cron. Once your cache is warm and your TTLs are tuned, a hit rate of 75-85% is achievable for apps with recurring video requests. At 80% you are saving 4 out of every 5 credits you would otherwise spend.

If you are building a pipeline that feeds transcripts into an LLM for summarization or fine-tuning, caching becomes even more valuable because those workloads routinely process the same source videos across multiple experiments. The Build a RAG Pipeline on YouTube Transcripts guide shows how to structure the downstream processing around cached transcript data.

Best-Practice Callouts

Keep your cache key deterministic. Avoid including timestamps, request IDs, or any value that varies between calls for the same logical resource. A non-deterministic key always misses.

Set a size limit. DiskCache accepts a size_limit argument. Without it, a long-running process can fill the disk with cached responses. A 10 GB ceiling is generous for transcript text and keeps the cache from becoming a liability.

Use compression for large transcript datasets. DiskCache compresses values with zlib by default when they exceed a threshold. For long-form transcripts, that typically yields 70-80% size reduction, which stretches your cache budget further.

Do not cache error responses. If the API returns a 404 (video not found) or 429 (rate limit), do not store that response. Cache only successful 200 responses. Storing errors means a transient failure becomes a permanent miss until the TTL expires.

Warm the cache in advance for known video sets. If you know a batch job will process a playlist tonight, pre-fetch and cache the transcripts during off-peak hours. This separates I/O from processing and makes the job deterministic.

Persist the cache across deployments. Store the DiskCache directory on a volume that survives container restarts. Losing the cache on every deploy wastes the credits spent building it.


Caching is one of the simplest interventions in API cost optimization and one of the most effective for transcript workloads specifically. Start with a flat 24-hour TTL and the composite key pattern, ship it, then iterate on the tiered TTL logic once you have hit-rate data from real traffic.

YouTube Transcriber starts every account with 100 free credits and requires no credit card to get started. The API documentation covers all endpoint parameters and response shapes you will need to build the patterns above into your own stack.