When building a YouTube scraper, you usually have two bad choices:
- The Official API: High latency, strict quotas (10k units vanish in seconds), and a heavy registration process.
- Browser Automation (Selenium/Playwright): Heavy, slow, and fragile.
I wanted a third way. A way that was fast, lightweight, and didn't require a Google Cloud project. That's how I ended up reverse-engineering InnerTube.
What is InnerTube? 🔍
InnerTube is the internal API that powers everything YouTube: the web app, mobile apps, and even smart TVs. It’s a unified endpoint (/youtubei/v1/...) that accepts JSON and returns massive, highly nested structures.
The challenge? It’s not documented.
The Performance Gap: Pure HTTP vs. Selenium
I ran a quick benchmark scraping comments from a trending video. The results were mind-blowing:
| Method | Time (100 comments) | Memory Usage | Reliability |
|---|---|---|---|
| Selenium (Headless) | ~8.5 seconds | ~450 MB | Low (DOM changes) |
| Official API | ~1.2 seconds | ~40 MB | High (but Quota-limited) |
| ytscrape (InnerTube) | ~0.4 seconds | ~35 MB | High |
By skipping the browser rendering engine and talking directly to the data layer, ytscrape is roughly 20x faster than browser automation.
The Technical Hurdles đźš§
Reverse-engineering this wasn't just about finding the right URL. Here are the three biggest challenges I had to solve in ytscrape:
1. The Context Object
InnerTube won't talk to you unless you provide a valid "context" — a complex object containing client versions, capability flags, and visitor data. I had to build a mechanism to extract this dynamically from the YouTube homepage to keep the library resilient.
2. Continuation Tokens (The Infinite Scroll)
YouTube doesn't use standard page numbers. It uses continuation tokens. Each response contains a encrypted string that you must send back to get the next chunk of data. Implementing this as a transparent Python generator was key:
# Transparent pagination in ytscrape
for comment in yt.comments(video_id):
# The library handles the token exchange behind the scenes
process(comment)
3. Typing the Chaos
The JSON returned by InnerTube is... messy. A single video title might be nested 8 levels deep. I used Python Dataclasses to map these structures into something developers actually want to use. No more data['contents'][0]['item']['text'].
Why Async Matters Here ⚡
Because ytscrape is pure HTTP, it plays incredibly well with asyncio. When you're fanning out to scrape 50 different video IDs, the official API would throttle you, and Selenium would crash your RAM. With AsyncYouTube, you can handle high-concurrency crawls with minimal resource footprint.
async with AsyncYouTube(max_concurrency=10) as yt:
tasks = [yt.video(url) for url in urls]
videos = await asyncio.gather(*tasks)
Lessons Learned
Building ytscrape taught me that sometimes the best API is the one that's already there, hidden in the Network tab of your browser.
If you’re interested in high-performance scraping or want to see how I mapped the InnerTube responses to Python models, check out the source code:
👉 GitHub: https://github.com/vsmutok/ytscrape
What’s your experience with internal APIs? Have you ever ditched an official SDK for a custom implementation? Let's discuss in the comments!
Top comments (2)
Nice write-up, especially the continuation-token-as-generator design.
One caveat from building something similar for the player endpoint (captions rather than comments): the results depended a lot on where it ran. From a small VPS it passed, but from a big-cloud container IP I got 1 transcript in 5, and from a datacenter proxy pool 0 in 8, mostly
LOGIN_REQUIRED("confirm you're not a bot"). A residential pool passed 28 of 28 videos. The ANDROID client context needed no key or PO token in my tests, while the WEB context wanted a PO token.Have you tried ytscrape from a server or CI IP? If it holds up there that's a big deal. If not, a line in the README would save people a surprise.
Thanks for the detailed feedback and insights — really appreciate it!
You're spot on regarding datacenter vs. residential IPs. Like most non-official InnerTube clients,
ytscraperuns into YouTube's bot-detection (LOGIN_REQUIRED/ consent walls) much more frequently when executed from major cloud providers (AWS, GCP, Hetzner) or CI runners compared to local residential connections.A few details on how
ytscrapecurrently handles this:ANDROIDclient context for theplayerendpoint when pulling caption tracks (inspired byyoutube-transcript-api), which helps bypass some of the WEB PO-token requirements for captions.WEBcontext and initial visitor token extraction from the homepage.proxies/ session config) and custom retry/rate-limiting policies to mitigate IP reputation issues.You're completely right about setting user expectations — adding a dedicated section in the README/docs about IP reputation, server/datacenter caveats, and recommended proxy setups will save people a lot of debugging time. I'll make sure to add that.
Thanks again for the great suggestion!