Caching YouTube Transcripts to Cut API Costs
David Boulen · 7/12/2026 · 9 min read
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.

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.

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.