The YouTube Data API gives a new project 10,000 quota units a day. That sounds like plenty until you notice that one search.list call costs 100 units and returns at most 50 results. If you use search to list a channel's videos, about 100 calls empty the whole day's quota.
You usually don't need search for that. This post shows the cheaper path and a short script that uses it.
Disclosure: I maintain the Apify Actor mentioned at the end. The script is free and stands on its own.
Where the quota goes
From Google's quota cost table (developers.google.com/youtube/v3/determine_quota_cost):
| Method | Cost per call | Max items per call |
|---|---|---|
search.list |
100 units | 50 |
channels.list |
1 unit | 50 |
playlistItems.list |
1 unit | 50 |
videos.list |
1 unit | 50 |
commentThreads.list |
1 unit | 100 |
Your project's quota page in Google Cloud is the authority for your actual limit. Check it before you plan a big job.
The trick: every channel has an uploads playlist
channels.list with part=contentDetails returns relatedPlaylists.uploads. That is a playlist holding every public upload of the channel, newest first. Reading it with playlistItems.list costs 1 unit per 50 videos, and videos.list then adds views, likes, comment count and duration for another 1 unit per 50.
So "latest 100 videos with stats" costs:
- 1 unit for the channel lookup (
forHandleaccepts an@handledirectly); - 2 units for two playlist pages;
- 2 units for two
videos.listbatches.
That's 5 units. The same job through search.list would be at least 200 units, plus the stats calls.
The script
Standard library only. Get a free API key in Google Cloud (enable YouTube Data API v3, then Credentials > Create credentials > API key) and put it in YOUTUBE_API_KEY.
import json, os, sys, urllib.parse, urllib.request
KEY = os.environ["YOUTUBE_API_KEY"] # your own key from Google Cloud
API = "https://www.googleapis.com/youtube/v3/"
units = 0
def call(method, cost, **params):
global units
units += cost
url = API + method + "?" + urllib.parse.urlencode({**params, "key": KEY})
with urllib.request.urlopen(url, timeout=30) as r:
return json.load(r)
def channel(handle):
"""1 unit: profile, stats and the hidden 'uploads' playlist ID."""
return call("channels", 1, part="snippet,statistics,contentDetails", forHandle=handle)["items"][0]
def latest_uploads(uploads_id, n=50):
"""1 unit per 50 videos, instead of 100 units per search.list page."""
ids, token = [], None
while len(ids) < n:
page = call("playlistItems", 1, part="contentDetails", playlistId=uploads_id,
maxResults=50, **({"pageToken": token} if token else {}))
ids += [i["contentDetails"]["videoId"] for i in page["items"]]
token = page.get("nextPageToken")
if not token:
break
return ids[:n]
def stats(video_ids):
"""1 unit per 50 videos: views, likes, comments, duration."""
out = []
for i in range(0, len(video_ids), 50):
out += call("videos", 1, part="snippet,statistics,contentDetails",
id=",".join(video_ids[i:i + 50]))["items"]
return out
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
c = channel(sys.argv[1] if len(sys.argv) > 1 else "@GoogleDevelopers")
print(c["snippet"]["title"], "| subscribers:", c["statistics"].get("subscriberCount"),
"| videos:", c["statistics"].get("videoCount"))
vids = stats(latest_uploads(c["contentDetails"]["relatedPlaylists"]["uploads"], n=100))
for v in vids[:3]:
print(v["snippet"]["publishedAt"][:10], v["statistics"].get("viewCount"), v["snippet"]["title"][:70])
print(len(vids), "videos with stats for", units, "quota units")
Real output
I ran it on 2026-11-09 against @GoogleDevelopers:
Google for Developers | subscribers: 2680000 | videos: 6125
2026-10-29 13867 Accelerate shipping to scale with Google Antigravity
2026-10-29 3253 ðŠī Building an autonomous plant rover with Gemini Live
2026-10-28 41415 Building real-time vision & edge navigation with Gemini Live
100 videos with stats for 5 quota units
Gotchas
- Subscriber counts are rounded. The API returns them rounded to three significant figures, which is why you see 2680000 rather than an exact number.
-
Uploads order is not strictly publish order. It's close, but scheduled and re-published videos can land out of order. Sort by
publishedAtif order matters. -
Private and deleted videos drop out.
videos.listsilently omits them, so you can get fewer stats rows than IDs. -
Missing stats.
likeCountis absent when the owner hides it, andcommentCountwhen comments are off. Use.get(). -
When you do need search. Keyword discovery across all of YouTube still needs
search.list. UsepublishedAfter/publishedBeforewindows, because a single query stops at about 500 results. - The 30-day rule. YouTube's developer policies require stored API data to be refreshed or deleted within 30 days.
If you would rather not maintain it
I publish a YouTube Data API Actor on Apify that uses these same quota-cheap calls, with your own key as a secret input. It covers channel profiles and uploads, playlists, video details, comments (with optional replies), keyword search and the trending chart per country. Every run saves a QUOTA_REPORT with the units it used.
Python (pip install "apify-client>=3", tokens in APIFY_TOKEN and YOUTUBE_API_KEY):
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("datawharf-tools/youtube-data-api").call(run_input={
"apiKey": os.environ["YOUTUBE_API_KEY"],
"mode": "channel",
"channel": "@GoogleDevelopers",
"maxResults": 100,
})
for row in client.dataset(run.default_dataset_id).iterate_items():
print(row.get("type"), row.get("title"), row.get("viewCount"))
It costs $0.40 per 1,000 rows plus standard Apify usage, and your Google quota stays on your own project. More examples, including JavaScript, are in the examples repo. If the script above covers your needs, use it.
Not affiliated with or endorsed by YouTube or Google.
New to Apify? If you start a paid plan through this referral link, I earn a commission at no extra cost to you.
Top comments (0)