<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tian song</title>
    <description>The latest articles on DEV Community by tian song (@tian_song).</description>
    <link>https://dev.to/tian_song</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172617%2F1701cc73-bfc1-482e-9d09-909e8f2bcfaf.png</url>
      <title>DEV Community: tian song</title>
      <link>https://dev.to/tian_song</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tian_song"/>
    <language>en</language>
    <item>
      <title>Reading a 40,000-comment YouTube thread with the Data API</title>
      <dc:creator>tian song</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:19:23 +0000</pubDate>
      <link>https://dev.to/tian_song/reading-a-40000-comment-youtube-thread-with-the-data-api-5dap</link>
      <guid>https://dev.to/tian_song/reading-a-40000-comment-youtube-thread-with-the-data-api-5dap</guid>
      <description>&lt;p&gt;Most tutorials that show you how to read YouTube comments stop at the&lt;br&gt;
first page. Here is what actually happens when you try to read a thread&lt;br&gt;
that has 40,000 of them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The endpoint, and what it actually costs
&lt;/h2&gt;

&lt;p&gt;Comments live behind &lt;code&gt;commentThreads.list&lt;/code&gt; on the YouTube Data API v3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://www.googleapis.com/youtube/v3/commentThreads
    ?part=snippet,replies
    &amp;amp;videoId=VIDEO_ID
    &amp;amp;maxResults=100
    &amp;amp;pageToken=NEXT_PAGE
    &amp;amp;key=YOUR_API_KEY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things bite people here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is quota-metered, not rate-limited.&lt;/strong&gt; &lt;code&gt;commentThreads.list&lt;/code&gt; costs 1 unit&lt;br&gt;
per call against a default 10,000-unit daily quota. At &lt;code&gt;maxResults=100&lt;/code&gt; that&lt;br&gt;
is 100 calls — and 100 calls is 1% of your day. A busy video with 40,000&lt;br&gt;
comments needs 400 calls. You will hit the ceiling faster than you expect,&lt;br&gt;
and the error is a plain &lt;code&gt;403 quotaExceeded&lt;/code&gt;, not a retryable 429.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxResults&lt;/code&gt; is capped at 100 and the cap is silent.&lt;/strong&gt; Ask for 500 and you&lt;br&gt;
get 100 with no warning. Loop until a page returns fewer items than you asked&lt;br&gt;
for, or you will spin forever on a video whose comment count is a multiple&lt;br&gt;
of 100.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pagination that actually terminates
&lt;/h2&gt;

&lt;p&gt;The naive loop breaks in three places: the last page, a video with comments&lt;br&gt;
disabled, and a video that is age-restricted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.googleapis.com/youtube/v3/commentThreads&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;part&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snippet,replies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;videoId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 403 covers both "comments disabled" and "quota exhausted".
&lt;/span&gt;        &lt;span class="c1"&gt;# Read the reason, do not just retry.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commentsDisabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quota or permission problem: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nextPageToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nextPageToken&lt;/code&gt; is absent on the final page — that is your exit&lt;br&gt;
condition. Do not compare against &lt;code&gt;pageInfo.totalResults&lt;/code&gt;; it is an estimate&lt;br&gt;
and it drifts on large threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replies are a second request
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;commentThreads.list&lt;/code&gt; returns the top-level comment plus, by default, a&lt;br&gt;
single reply. Getting the rest means calling &lt;code&gt;comments.list&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;parentId&lt;/code&gt;. On a thread with heavy back-and-forth this doubles or triples&lt;br&gt;
your quota spend, and there is no way to know in advance how much.&lt;/p&gt;

&lt;p&gt;If you only need the top-level conversation, do not request replies at all —&lt;br&gt;
you get roughly 3x the coverage for the same quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you do not want to write any of this
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://apriocity.com" rel="noopener noreferrer"&gt;Apriocity&lt;/a&gt; because I kept writing this same&lt;br&gt;
pagination loop for one-off research tasks. It does the paging, the quota&lt;br&gt;
accounting and the reply expansion for you, and hands back CSV or JSON:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paste a video link, read the whole thread, search it by keyword&lt;/li&gt;
&lt;li&gt;export the slice as CSV or JSON&lt;/li&gt;
&lt;li&gt;batch mode: up to ten videos, one ZIP back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It uses the same public API described above, so what you see is exactly&lt;br&gt;
what is publicly visible — nothing is stored on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;commentThreads.list&lt;/code&gt; = 1 quota unit per call, &lt;code&gt;maxResults&lt;/code&gt; capped at 100&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nextPageToken&lt;/code&gt; missing = done; do not trust &lt;code&gt;totalResults&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;403&lt;/code&gt; is overloaded: check the &lt;code&gt;reason&lt;/code&gt; field before retrying&lt;/li&gt;
&lt;li&gt;Replies cost extra calls — skip them unless you need them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are doing this once, the API is fine. If you are doing it weekly,&lt;br&gt;
that loop is worth automating away.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>youtube</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
