Google's YouTube Data API pages through a video's comments: commentThreads.list costs 1 unit per call and returns up to 100 threads per page, inside a default 10,000 units a day. Its docs print no depth limit, and we did not test one. A thread may carry only some of its replies. We read Google's documentation on 2026-10-17. ToolzerHub returns about 20 top-level comments per call and no reply text; its default order stops after about 50 pages, sort_order=newest reached the end on three videos, and some videos return an empty list.
What Google's API returns for a video
commentThreads.list takes a videoId and a part of id, snippet or replies. Google's reference (last updated 2026-10-15): "Quota impact: A call to this method has a quota cost of 1 unit." maxResults takes "1 to 100, inclusive. The default value is 20."
Comments turned off return 403 commentsDisabled: "The video identified by the videoId parameter has disabled comments." An unknown video returns 404 videoNotFound.
You need a Google Account, a project and an API key. We did not call Google's API ourselves; this is from its documentation.
Replies need a second call
A thread's replies field is partial. Google's commentThreads page says: "The commentThread resource does not necessarily contain all replies to a comment, and you need to use the comments.list method if you want to retrieve all replies for a particular comment." The list is "only a subset" unless its length equals snippet.totalReplyCount.
comments.list with parentId returns the replies of one top-level comment, also 1 unit and 1 to 100 per page. So you page the threads, then call comments.list for each thread whose replies list is shorter than totalReplyCount. Google doesn't say how many replies a thread holds inline.
Quota for a big job
Google's quota page: "Projects that enable the YouTube Data API have a default quota allocation of 100 search.list calls, 100 videos.insert calls, and 10,000 units per day combined for all other endpoints." Both comment methods cost 1 unit in its table, every extra page costs again, and "All API requests, including invalid requests, incur a quota cost of at least one point."
More quota needs an audit first: "you must first complete an audit to show that your project is in compliance with the YouTube API Services Terms of Service." The pages we read print no price. Storage is limited too: of Non-Authorized Data, "API Data accessible by an API Client without User Credentials", you may store limited amounts for "not longer than 30 calendar days", then delete or refresh it.
Get a video's comments with ToolzerHub
We make ToolzerHub. Our YouTube API returns a public video's comments as JSON with one API key and no Google project. GET /v2/youtube/comments takes video_reference (an ID or a watch, youtu.be, Shorts or embed URL), sort_order (top or newest) and page_cursor. It costs 1 credit per call (our OpenAPI file, 2026-10-17).
Each comment has commentId, textDisplay, likesCount, replyCount, publishedAt and the author's channel ID and handle. The first page also carries commentsCount, YouTube's total for the video, replies included. Replies are not returned: you get the count and no text, and we have no route for them. Likes from 1,000 up come back as rounded text such as 1.8K; the script turns it into a number (1800), so large counts are approximate.
Save this as youtube_comments.py. It writes one row per comment, without the author.
import csv
import os
import sys
import time
import requests
URL = "https://api.toolzerhub.com/v2/youtube/comments"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["video", "comment_id", "published", "likes", "replies", "text"]
calls = 0
FAILED = object() # marks a 400 or a 503 that outlasted the retries, so the run can tell it from "empty"
def cell(value):
# Spreadsheets run text that starts with = + - or @ as a formula; an apostrophe keeps it text.
if isinstance(value, str) and value[:1] in ("=", "+", "-", "@", "\t", "\r"):
return "'" + value
return value
def num(value):
# Counts of 1,000 and up can come back as text such as "1.8K".
text = str(value or "0").upper().replace(",", "")
try:
return int(float(text[:-1]) * 1000) if text.endswith("K") else int(text)
except ValueError:
return 0
def get(params, tries=4):
# 429 and 503 are retryable; the wait comes from the error body or the Retry-After header.
global calls
for attempt in range(tries):
r = requests.get(URL, headers=HEADERS, params=params, timeout=90)
calls += 1
if r.status_code == 400: # malformed video id: skip this video, keep the rest of the run
print("bad request:", r.json()["error"]["message"].splitlines()[0])
return FAILED
if r.status_code not in (429, 503) or attempt == tries - 1:
if r.status_code in (429, 503): # still failing after the retries: give up on this video
return FAILED
if r.status_code >= 400:
sys.exit(f"HTTP {r.status_code}: {r.text[:200]}")
return r.json().get("data") # None when there is nothing to return
try:
wait = r.json()["error"]["details"]["retryAfter"]
except (ValueError, KeyError, TypeError):
wait = None
try:
wait = int(wait or r.headers.get("Retry-After") or 5 * (attempt + 1))
except ValueError:
wait = 5 * (attempt + 1)
time.sleep(min(wait, 60))
def export_video(writer, video, max_pages, sort):
params = {"video_reference": video, "sort_order": sort}
seen, pages, more, failed, total, replies = set(), 0, False, False, None, 0
while pages < max_pages:
data = get(params)
if data is FAILED:
failed = True
break
if not data or not data.get("data"):
break # null data, comments turned off, no comments, or an unknown id
pages += 1
total = total or data.get("commentsCount")
for c in data["data"]:
if c["commentId"] in seen: # keep one row per comment id
continue
seen.add(c["commentId"])
replies += num(c.get("replyCount"))
writer.writerow({
"video": video, "comment_id": c["commentId"],
"published": (c.get("publishedAt") or "")[:10],
"likes": num(c.get("likesCount")), "replies": num(c.get("replyCount")),
"text": cell((c.get("textDisplay") or "").replace("\n", " ")),
})
more = bool(data.get("page_cursor"))
if not more:
break
params["page_cursor"] = data["page_cursor"]
time.sleep(0.4) # the free plan allows 3 requests a second
if failed:
status = " (gave up: bad request or repeated 429/503)"
elif not pages:
status = " (nothing returned: comments off, none yet, or unknown id)"
elif more:
status = " (stopped at the page cap, more available)"
elif sort == "top":
status = " (top order ended early; use newest for the full list)"
else:
status = f" (reached the end; {replies} replies not fetched; YouTube shows {total} in all)"
print(f"{video}: {len(seen)} comments, {pages} pages{status}")
return len(seen)
def main(videos, max_pages="50", sort="newest", out="comments.csv"):
total = 0
with open(out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for video in [v.strip() for v in videos.split(",") if v.strip()]:
total += export_video(writer, video, int(max_pages), sort)
print(f"wrote {total} comments to {out} in {calls} calls")
if __name__ == "__main__":
if len(sys.argv) < 2 or (len(sys.argv) > 3 and sys.argv[3] not in ("newest", "top")):
sys.exit("usage: python youtube_comments.py VIDEO_ID_OR_URL[,...] [max_pages] [newest|top] [file.csv]")
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
A 400 skips that video, 429 and 503 are retried and then that video is skipped, and a 200 with {"data": null} or an empty list is reported as nothing returned. It de-duplicates by commentId and stops at the end or at your page cap. cell() puts an apostrophe in front of text that starts with =, +, - or @, so a spreadsheet shows it as text. We tested the retry branches against a stand-in server, not live.
Run the script
export TOOLZERHUB_API_KEY=your_key
python youtube_comments.py "GUqsH5y1j1M, https://www.youtube.com/watch?v=1C_zuHf6lP4 ,zzzzzzzzzzz,,bad id,_eeZQw9PBc0" 100 newest comments.csv
Output on 2026-10-17, with a stray space, a URL, an unknown ID, an empty item, an invalid ID and a NASA video that returns no comments added to test the handling:
GUqsH5y1j1M: 1288 comments, 65 pages (reached the end; 877 replies not fetched; YouTube shows 2165 in all)
https://www.youtube.com/watch?v=1C_zuHf6lP4: 1649 comments, 83 pages (reached the end; 710 replies not fetched; YouTube shows 2359 in all)
zzzzzzzzzzz: 0 comments, 0 pages (nothing returned: comments off, none yet, or unknown id)
bad request: video_reference must be a valid YouTube video ID or URL
bad id: 0 comments, 0 pages (gave up: bad request or repeated 429/503)
_eeZQw9PBc0: 0 comments, 0 pages (nothing returned: comments off, none yet, or unknown id)
wrote 2937 comments to comments.csv in 151 calls
It took 108 seconds, including a 0.4 second pause after each page, and at most 151 credits, $0.15 to $0.29. We don't show rows from the file: the comments are ordinary people's. Comments are personal data about people who never signed up for your project, so collect only the fields you need and delete the file when the job is done.
How deep it goes
On 2026-10-17 we paged four public videos, once per sort order. "Total" is commentsCount from the first page. "Replies" is the sum of replyCount over the newest rows.
| Video | Total | Newest | Replies | Top |
|---|---|---|---|---|
| GUqsH5y1j1M | 2,165 | 1,288 | 877 | 991 |
| 1C_zuHf6lP4 | 2,359 | 1,649 | 710 | 962 |
| rzmd7RouGrM | 3,336 | 2,010 | 1,325 | 985 |
| UF8uR6Z6KLc | 29,253 | 3,000+ | 1,212 |
On the first three, newest ended on its own and rows plus reply counts match YouTube's total within 1: 1,288 + 877 = 2,165, 1,649 + 710 = 2,359, and 2,010 + 1,325 = 3,335 against 3,336. The replies are the missing part, 877 of 2,165 on the first video. On the 29,253-comment video we stopped newest at our 150-page cap, 3,000 comments with more available.
The top order ended after 49 to 61 pages on every video, and the default order behaves the same; a default run on the first video gave 995 unique comments in 50 pages. For a complete list, send sort_order=newest. We don't know why top stops.
A page holds 20 comments; one or two pages per run held fewer, as few as 2. No id repeated within a run. Dates look derived from text such as "5 years ago", so older comments have an approximate day.
On 5 of 8 videos from NASA's channel the route returned 200 with an empty list. We did not find out why. An unknown ID returns the same empty list, so the response usually cannot tell comments turned off from a wrong ID; one of the five returned commentsCount: "0". Google's 403 and 404 errors can.
What it costs against Google's units
| Job | ToolzerHub | |
|---|---|---|
| 1,000 top-level comments | 10 units | about 50 credits |
| Video with 1,288 threads | 13 units | 65 credits |
| Replies on that video | up to 240 units | not returned |
| Allowance | 10,000 units a day | 100 credits a month |
Google's 13 is 1,288 threads at 100 per page; its replies row is one comments.list call per 100 replies for each of the 239 threads with replies (one thread has 104), fewer if a thread's inline replies are already complete. We took the counts from our own run and did not call Google. Ours is about 50 credits per 1,000 comments, $0.05 to $0.095 at $0.99 to $1.90 per 1,000 credits. The free 100 credits cover about 2,000 comments a month.
Where Google's API is the better pick
Google's default quota is far larger: 10,000 calls a day against 100 credits a month. Use it when you need the replies, 100 comments per call, a clear error for disabled comments or an unknown video, every thread on a channel with allThreadsRelatedToChannelId, searchTerms to filter, or posting and moderation with your own authorization.
Use ours when you have no Google project, or don't want one, and top-level comments are enough. For other YouTube limits see the YouTube search API quota and how to get a video's transcript.
We are not affiliated with Google or YouTube.
Sources
- Google, CommentThreads: list, https://developers.google.com/youtube/v3/docs/commentThreads/list (last updated 2026-10-15), read 2026-10-17
- Google, CommentThreads resource, https://developers.google.com/youtube/v3/docs/commentThreads (last updated 2026-10-15), read 2026-10-17
- Google, Comments: list, https://developers.google.com/youtube/v3/docs/comments/list (last updated 2026-10-15), read 2026-10-17
- Google, Quota costs for API requests, https://developers.google.com/youtube/v3/determine_quota_cost (last updated 2026-10-15), read 2026-10-17
- Google, Quota and Compliance Audits, https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits (last updated 2026-10-15), read 2026-10-17
- Google, Getting started, https://developers.google.com/youtube/v3/getting-started (last updated 2026-10-15), read 2026-10-17
- Google, YouTube API Services Developer Policies, https://developers.google.com/youtube/terms/developer-policies (last updated 2026-10-15), read 2026-10-17
- ToolzerHub OpenAPI file, https://api.toolzerhub.com/openapi.json, checked 2026-10-17
- ToolzerHub pricing, https://toolzerhub.com/pricing, checked 2026-10-17