YouTubeAnswer

YouTube API get all comments: Google's limits and a paging script

Google's API returns up to 100 comments a call for 1 unit, with partial replies. Ours gives ~20, no replies; newest reaches the end on videos up to 3.3k.

10 min read

Short answer

  • The official YouTube Data API pages through a video's comments within its default quota: commentThreads.list costs 1 unit per call and returns up to 100 threads, and the default 10,000 units a day pays for 10,000 calls. A thread may hold only some of its replies; the rest need comments.list with parentId (developers.google.com, read 2026-10-17).
  • ToolzerHub's /v2/youtube/comments returns top-level comments only, about 20 per call, with a replyCount and no reply text. With sort_order=newest we paged three videos to the end (1,288 to 2,010 comments). The default top order stopped after 49 to 61 pages, at 962 to 1,212 comments. 5 of 8 NASA videos returned an empty list, which looks the same as an unknown ID.
  • 1 credit per call, about 50 credits per 1,000 comments: $0.05 to $0.095 at $0.99 to $1.90 per 1,000 credits. 100 free credits a month. The script below writes comments to CSV with a page cap.
On this page

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.

python
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

bash
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:

text
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 Google 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

Get this data from your own code

The YouTube API returns it as JSON with one x-api-key header. 100 free credits monthly.