Reddit Post Comments Scraper — Full Comment Tree, Any Post URL avatar

Reddit Post Comments Scraper — Full Comment Tree, Any Post URL

Pricing

$1.50 / 1,000 per comment returneds

Go to Apify Store
Reddit Post Comments Scraper — Full Comment Tree, Any Post URL

Reddit Post Comments Scraper — Full Comment Tree, Any Post URL

Reddit comments scraper: paste post URLs (permalinks, redd.it, share links, comment links) and get every comment as flat rows with parent id, depth, thread path, reply and descendant counts, author, score, timestamps. Expands 'load more' stubs. No login or API key.

Pricing

$1.50 / 1,000 per comment returneds

Rating

0.0

(0)

Developer

Scrapers Delight

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Paste Reddit post URLs and get every comment under them as flat rows that still carry the tree. One row per comment: body, author, score, depth, parent_comment_id, thread_path, reply_count, descendant_count, position, created_iso, edited, permalink, is_deleted and the post context on every row (post_title, post_url, post_score, post_num_comments, tree_complete). "Load more comments" and "continue this thread" stubs are expanded through Reddit's own morechildren and subtree endpoints, so a big thread comes back whole, not as the first page plus a pile of stubs. No login. No Reddit API key. No CAPTCHA solving.

Scope, measured 2026-08-22 through the Apify proxy: a thread Reddit counts at 1,323 comments was delivered as 1,359 rows in 124 s with 92 Reddit calls, tree_complete: true — Reddit's own first page of that thread shows only 479 comments and hides the rest behind 189 "load more" stubs, and a single bulk morechildren call serves only ~60 of every 100 ids it is asked for (this Actor goes back for the rest). Raw HTTP gets 0 of 66 requests through Reddit's challenge on every proxy tier; a real browser on residential-US got 10/10 comment trees across 5 fresh sessions. That transport is what you are paying for here.

{
"postUrls": ["https://www.reddit.com/r/programming/comments/luq9oz/how_i_cut_gta_online_loading_times_by_70/"],
"commentSort": "top",
"maxCommentsPerPost": 200
}

Click Try for free and hit Start — that block is literally the input the Actor ships with. A run on those shipped defaults returned 200 rows in 19 s (one browser session, one Reddit call) for $0.30, with body 100%, author 100%, score 100%, depth 100%, parent id 100% on nested rows, and reply_count / descendant_count computed on the whole fetched tree.


The wedge: a tree you can actually use, and a row that tells you if it is complete

Twenty-plus Actors sell Reddit comments. Most hand you Reddit's JSON fields as-is and stop at the first page. Three things are different here:

  1. Tree fields that are computed, not copied. Reddit's JSON gives you parent_id and depth. This Actor adds thread_path (root-first list of ancestor comment ids, so one JOIN rebuilds any branch), reply_count (direct replies present) and descendant_count (whole subtree size), is_top_level and position (delivery order in the sort you chose). "Which top-level comment started the biggest argument" is ORDER BY descendant_count DESC WHERE is_top_level — no recursion on your side.
  2. Expansion of the hidden part of the thread — including the ids Reddit drops. On a 1,323-comment post Reddit's listing endpoint returns 479 comments + 189 stubs (measured). With expandMoreComments on (default) the stubs are pooled into morechildren calls and "continue this thread" cuts are fetched as subtrees. Reddit's morechildren also silently drops ids when one call's reply budget is used up (measured: 100 ids asked, 59 served; the same ids re-asked in smaller batches came back). This Actor re-queues every unserved id at a smaller batch size, down to asking for it alone, so "the first page plus whatever one bulk call felt like returning" never passes as complete.
  3. tree_complete on every row, and a post_num_comments to check it against. You never have to guess whether a thread was truncated by a cap, the time budget or a stub that would not expand — the row says so, and the log prints delivered of N reddit counts per post.

Also: any URL shape Reddit produces is accepted — desktop permalinks, redd.it short links, the mobile app's /r/sub/s/AbCdEfG share links (resolved live inside the page), profile post links (/user/name/comments/…), old. / new. / np. subdomains, bare ids, and comment permalinks: paste a link to one comment and you get that comment's thread only (scope: "comment-thread", focus_comment_id set).


Read this before you buy rows

  1. post_num_comments is Reddit's counter, not a promise. It is maintained separately from the tree and drifts in both directions: the measured "1,323-comment" thread delivered 1,359 rows (Reddit still serves [deleted] placeholders the counter dropped), while 49 ids that Reddit's stubs named were not served even when asked for one at a time (removed by spam filters — counted per post in comments_absent_at_source). tree_complete: true means every stub Reddit offered was expanded and every id it named was asked for, not that the two numbers match.
  2. The per-post cap cuts after the tree math. With maxCommentsPerPost: 200 on a 992-comment post you get the first 200 rows in top order, but each of those rows still carries the reply_count / descendant_count of its full fetched subtree. thread_path can name an ancestor that is not in your 200 rows — it is an id, not a join failure.
  3. [deleted] and [removed] rows are real rows. Reddit keeps them so the tree stays connected (8.8% of the 740-row sample). They are delivered with is_deleted: true and billed like any other row; set includeDeleted: false to drop them unbilled.
  4. Score is a snapshot and score_hidden is true on very fresh comments (Reddit hides the score for the first minutes-to-hours). Re-run later if you need settled scores.
  5. A browser session costs ~13 s before the first row. Posts in one run share that session (default 40 posts per session). A one-post run is ~25 s wall time; 100 posts are not 100× that.

What you get

One row per unique comment, deduplicated on Reddit's fullname (t1_…) across the whole run. Timestamps are ISO-8601 UTC plus the raw created_utc epoch. edited is false, or the ISO time of the edit.

GroupFieldsExample
Identityid, name, post_id, permalink, subreddit, subreddit_idnf7ge1o · t1_nf7ge1o · 1nlqi0c
Treeparent_id, parent_comment_id, is_top_level, depth, position, reply_count, descendant_count, thread_path[]depth 2 · 8 replies · 28 descendants · ["nf7d1fx","nf7etqb"]
Authorauthor, author_fullname, author_flair_text, author_is_op, author_premiumLividLife5541 · t2_1l047qectw
Contentbody, body_length, is_deleted, edited, distinguished, stickied, locked, collapsed, collapsed_reason183 chars
Votesscore, ups, score_hidden, controversiality, total_awards_received, gilded394
Timecreated_utc, created_iso2025-09-20T06:48:41.000Z
Post contextpost_title, post_url, post_author, post_subreddit, post_score, post_upvote_ratio, post_num_comments, post_created_iso, post_link_flair_text, post_is_self, post_external_url, post_selftext_preview, post_over_18, post_locked, post_archived1323 comments · ratio 0.96
Provenancesource (listing / more-children / continue-thread), scope, focus_comment_id, comment_sort, tree_complete, comments_delivered_for_post, comments_absent_at_source, input_url, scraped_at

The dataset ships with a Comments table view (post, depth, author, score, body, replies, thread size, posted, tree complete, link) so you do not have to configure columns.


Field fill — measured on 740 comments

A 120-second budgeted run on the 1,323-comment thread above, 2026-08-22, through Apify RESIDENTIAL-US: 479 rows from the listing, 258 from morechildren expansion, 3 from "continue this thread" subtrees. Sorted by fill, so the sparse fields are impossible to miss.

FieldFillNotes
id / name / post_id / parent_id / permalink100%
body / body_length100%[deleted] / [removed] placeholders included (see below)
author100%[deleted] for deleted accounts — still a string
score / ups / controversiality / gilded / total_awards_received100%
created_utc / created_iso100%
depth / position / reply_count / descendant_count100%115 top-level rows; raw depth runs 0–9, and the thread reaches depth 10 (see the continue-thread depth caveat)
post_* context (title, url, score, num_comments, created, author, ratio)100%
source / scope / comment_sort / input_url / scraped_at100%
comments_delivered_for_post100%integer, same value on every row of a post
comments_absent_at_source100%integer, same value on every row of a post (added after this sample was captured; 0 on the shipped-default run, 49 on the full run above)
author_fullname91.2%absent on deleted accounts
parent_comment_id / thread_path84.5%by definition null/empty on the 15.5% top-level rows
is_top_level15.5%a true boolean count, not a gap
is_deleted8.8%deleted + moderator-removed placeholders
edited3.6%ISO time when edited, false otherwise
collapsed_reason3.5%e.g. DELETED, LOW_SCORE
author_premium1.6%
collapsed1.4%
author_flair_text0.0%r/programming does not use user flair; subreddits that do fill it
author_is_op0.0%the OP did not reply in this thread — a real zero
distinguished / stickied / locked / score_hidden0.0%no mod comments, nothing pinned, scores settled
post_selftext_preview / post_link_flair_text0.0%a link post has no selftext; self posts fill it (first 500 chars)
tree_complete0.0%this run was budget-stopped at 120 s on purpose — the full run of the same post is what the scope line above reports

Every run prints a measured fill line in the log for the 12 fields most likely to vary by subreddit (body, author, score, created_iso, depth, parent_comment_id, author_flair_text, permalink, edited, distinguished, collapsed_reason, author_fullname), so you can check this table against your own subreddit.


How to run it

1. One or many post URLs (the usual choice)

{
"postUrls": [
"https://www.reddit.com/r/programming/comments/1nlqi0c/microsoft_asks_all_its_foreign_staff_to_return_to/",
"https://redd.it/144sn8r",
"https://www.reddit.com/r/AskReddit/s/AbCdEfGhIj",
"t3_luq9oz"
],
"commentSort": "top",
"maxCommentsPerPost": 0
}

maxCommentsPerPost: 0 = the whole tree. Share links are resolved to the canonical permalink inside the browser session and the log prints the resolution.

2. One comment's thread only

{ "postUrls": ["https://www.reddit.com/r/programming/comments/1nlqi0c/_/nf7d1fx/"] }

A comment permalink scopes the scrape to that comment and everything under it. Rows carry scope: "comment-thread" and focus_comment_id: "nf7d1fx"; depth is Reddit's own field and the focus comment is not renumbered to depth 0 (see the depth caveat under Fields people misread).

3. Top-level comments and their direct replies

{ "postUrls": ["https://redd.it/luq9oz"], "maxDepth": 1, "maxCommentsPerPost": 0 }

maxDepth filters on the 0-based depth field, so maxDepth: 1 returns depths 0 and 1 — 364 of the 1,359 rows on the thread above. It filters after the tree math, so each row still carries reply_count and descendant_count for the branch you did not buy. Filtered rows are never billed.

4. Page one only, fastest and cheapest

{ "postUrls": ["https://redd.it/luq9oz"], "expandMoreComments": false }

Exactly one Reddit call per post (up to ~500 comments). tree_complete is honest about what you skipped.

Scheduling and integrations

Save the input as a Task, attach a Schedule to re-pull a thread as the discussion grows, and read the dataset over the REST API or the standard integrations (Zapier, Make, n8n, webhooks, MCP). Starting a run from the API:

curl -X POST "https://api.apify.com/v2/acts/scrapersdelight~reddit-post-comments-scraper/runs?token=YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"postUrls":["https://redd.it/luq9oz"],"maxCommentsPerPost":0}'

Sample row

A real row from an actual run, captured 2026-08-22 17:28 UTC.

{
"id": "nf7ge1o",
"name": "t1_nf7ge1o",
"post_id": "1nlqi0c",
"parent_id": "t1_nf7etqb",
"parent_comment_id": "nf7etqb",
"is_top_level": false,
"depth": 2,
"position": 3,
"reply_count": 8,
"descendant_count": 28,
"thread_path": ["nf7d1fx", "nf7etqb"],
"author": "LividLife5541",
"author_fullname": "t2_1l047qectw",
"author_flair_text": null,
"author_is_op": false,
"author_premium": false,
"body": "I once sent a guy to Australia because he didn't win the (stupid) \"visa lottery\". He very much enjoyed being paid a US programmer's salary in Australia. Not sure if ever came back lol",
"body_length": 183,
"is_deleted": false,
"score": 394,
"ups": 394,
"score_hidden": false,
"controversiality": 0,
"total_awards_received": 0,
"gilded": 0,
"created_utc": 1758350921,
"created_iso": "2025-09-20T06:48:41.000Z",
"edited": false,
"distinguished": null,
"stickied": false,
"locked": false,
"collapsed": false,
"collapsed_reason": null,
"subreddit": "programming",
"subreddit_id": "t5_2fwo",
"permalink": "https://www.reddit.com/r/programming/comments/1nlqi0c/microsoft_asks_all_its_foreign_staff_to_return_to/nf7ge1o/",
"source": "listing",
"post_title": "Microsoft asks all its foreign staff to return to US by Sunday after Trump's H1-B bombshell",
"post_url": "https://www.reddit.com/r/programming/comments/1nlqi0c/microsoft_asks_all_its_foreign_staff_to_return_to/",
"post_author": "balianone",
"post_subreddit": "programming",
"post_score": 9152,
"post_upvote_ratio": 0.96,
"post_num_comments": 1323,
"post_created_utc": 1758348468,
"post_created_iso": "2025-09-20T06:07:48.000Z",
"post_link_flair_text": null,
"post_is_self": false,
"post_external_url": "https://economictimes.indiatimes.com/nri/work/microsoft-urges-h-1b-visa-holders-to-stay-in-us-for-foreseeable-future-after-trump-announces-100000-visa-fee/articleshow/124009637.cms?from=mdr",
"post_selftext_preview": null,
"post_over_18": false,
"post_locked": false,
"post_archived": false,
"scope": "post",
"focus_comment_id": null,
"comment_sort": "top",
"tree_complete": false,
"comments_delivered_for_post": 740,
"comments_absent_at_source": 0,
"input_url": "https://www.reddit.com/r/programming/comments/1nlqi0c/",
"scraped_at": "2026-08-22T17:28:21.970Z"
}

Fields people misread:

  • depth is 0-based — top-level comments are depth 0, and maxDepth: 1 keeps depths 0 and 1. It is Reddit's own field: absolute for rows with source: "listing" or "more-children", but rows fetched from a "continue this thread" subtree (source: "continue-thread") carry Reddit's depth relative to that subtree's root — on the measured 1,359-row thread that is 95 rows (7.0%), and their true depth is thread_path.length. Use thread_path.length when you need absolute depth, and note that maxDepth filters on the raw depth field.
  • descendant_count counts the whole subtree that was fetched, reply_count only direct children. On a capped or budget-stopped post both are lower bounds.
  • position is the delivery order: Reddit's first listing page in the sort you chose (that part is page reading order), then the expanded rows in the order the morechildren / subtree calls returned them. A parent always has a lower position than its children, but expanded branches are not interleaved into reading order — rebuild reading order from thread_path if you need it.
  • edited is false when never edited, the ISO time of the edit when Reddit gives a timestamp, and bare true on the rare legacy comment where Reddit reports the edit without one (0 of 1,359 rows on the measured thread).

Input

Fields in the order the Console shows them.

FieldTypeDefaultWhat it does
postUrlsstring list1 sample postPermalinks, redd.it, /s/ share links, profile-post links, comment permalinks, bare ids. One post pasted twice is fetched once.
commentSortselecttoptop, best, new, controversial, old, qa — decides which rows come first under a cap.
💷 Limits & cost
maxCommentsPerPostinteger200Per post. 0 = the whole tree.
maxTotalCommentsinteger0Hard cap for the run. 0 = none. The run's Max total charge is honoured to the whole row as well.
🌳 Tree depth & filters
expandMoreCommentsbooleantrueExpand "load more" / "continue this thread" stubs. Off = page one only.
maxDepthinteger0Keep depth ≤ N (0-based). 0 = unlimited. Filtered rows are never billed.
includeDeletedbooleantrueOff drops [deleted] / [removed] placeholders, unbilled.
⚙️ Proxy & performance
proxyConfigurationproxyApify RESIDENTIAL, country USRequired — see the transport table.
postsPerSessioninteger40Posts served per challenge-cleared browser session before a fresh exit IP is minted.
requestDelayMsinteger300Pacing between the expansion calls.

Pricing

$0.0015 per comment returned — $1.50 per 1,000. Charged on the comment-scraped event. No monthly fee from this Actor.

RunCommentsCost
The shipped default (1 post, 200 cap)200$0.30
The full thread above (1,359 rows)1,359$2.04
5 posts × ~260 comments (a typical VoC pull)1,300$1.95
10,000 comments of a mega-thread10,000$15.00
  • You are charged for rows delivered. Duplicates (same post pasted twice, overlapping comment threads), rows removed by maxDepth / includeDeleted, and posts that failed or 404'd are never billed.
  • Rows are charged as they are pushed (Actor.pushData(items, 'comment-scraped')), one post at a time, so a budget cap or time stop leaves you with whole posts' worth of rows, every one of them billed, none unbilled.
  • maxCommentsPerPost × number of posts is your cost ceiling; maxTotalComments and the run's Max total charge are hard caps on top. A charge cap too low for even one comment fetches nothing and bills nothing.

Per-comment pricing, deliberately. The nearest incumbent bills $0.00299 per post plus $0.00099 per comment; on a big thread that lands below this Actor's flat $0.0015 per comment, and what you buy here instead is the expansion of the hidden part of the tree, the computed tree fields, and a tree_complete flag that tells you when a thread was truncated. The browser-plus-residential transport that gets through Reddit's wall costs per byte, not per post.


Honest limits

  • Reddit's comment counter is not the tree. post_num_comments drifts both ways — the measured thread counted 1,323 and served 1,359 rows, while 49 ids its own stubs named were never served even when asked for alone. tree_complete reports whether every stub was expanded and every id asked for, which is the thing this Actor controls; comments_absent_at_source counts the ids Reddit would not serve.
  • No user profiles, no subreddit listings, no search. This Actor does one thing: post URL → comment tree. Our Reddit Subreddit Scraper lists a subreddit's posts (and can attach first-page comments).
  • No body HTML, no media. body is Reddit's markdown source (raw_json=1, so & is &). Images/GIFs in comments appear as their Reddit URLs inside body.
  • Private, quarantined and banned subreddits 404, as do deleted posts. The run logs the URL and moves on; if every post 404s the run ends SUCCEEDED with 0 rows and a status message saying so, so a wrong paste does not cost you a FAILED run.
  • A residential browser session is the price of admission. ~13 s to mint, about 2 GB of memory while it runs. Datacenter proxies do not clear Reddit's challenge (measured 0/3 even with a real browser), so the proxy setting is not a cost you can tune away.
  • Reddit's robots.txt says User-agent: * Disallow: / for both www.reddit.com and old.reddit.com, citing Reddit's Public Content Policy. This Actor reads logged-out public pages and Reddit's public JSON endpoints only; nothing behind a login is touched. Whether and how you may use the data commercially is between you and Reddit's terms — see Legal & fair use below.

How it works, and what it cost to make reliable

Reddit refuses raw HTTP with its own ~190 KB JS-challenge block page from every IP tier — it is a client-execution check, not IP reputation, and it is not Cloudflare, DataDome or PerimeterX. So this Actor launches a real Chromium with a desktop user agent (the default headless UA is the single decisive failure factor — swapping it for a desktop UA is what turned a run of failures into 20/20 challenge clears), lets the challenge settle (~11 s), then reads Reddit's own JSON endpoints with an in-page fetch(): /comments/<id>/.json for the listing, /api/morechildren.json for "load more" stubs pooled 50 ids per call (then 20, 6 and 1 for the ids a call dropped), and /comments/<id>/_/<commentId>/.json for "continue this thread" cuts. Structured JSON, never DOM scraping, one browser session per ~40 posts.

Transport ladder, measured 2026-08-22 through Apify, fresh session per call:

RungListing .jsonNotes
Raw HTTP, direct0 / 12403, Reddit's snooserv JS-challenge block page
Raw HTTP, Apify datacenter0 / 18403 (www) / "Blocked" page (old.reddit)
Raw HTTP, Apify RESIDENTIAL0 / 18403 same page; old.reddit serves a login shell with 0 comments
Raw HTTP, RESIDENTIAL + country US0 / 18same
Chromium + desktop UA, Apify datacenter0 / 3challenge never clears
Chromium + desktop UA, RESIDENTIAL + US, in-page fetch10 / 105 fresh sessions, 2,602 comments, raw depth ≤ 9

Sustained load on the shipped rung, this build:

TestResult
Full tree of the "1,323-comment" post, one session1,359 rows, 92 calls, 124 s, 0 failed calls, tree_complete true
120-second budgeted run, same post, hard-killed at 130 sexit 0, 740 rows delivered + budget status message, 86 s
Shipped default (1 post, 200 cap)200 rows, 19 s

Reddit's limit parameter on the listing endpoint tops out at ~500 comments per call (limit=1000 returned the same 479 as limit=500), which is why the expansion step exists.


Duplicates — measured in both directions

Within a post, Reddit returns each comment once across the listing and the expansion calls; the Actor keeps a per-post set and a run-wide set keyed on the t1_ fullname, drops repeats before billing, and asserts uniqueness across everything it pushed before the run is allowed to succeed. Measured: 740 rows → 740 distinct ids (0.0%); 477 rows → 477 distinct (0.0%). Duplicates do arise from input — the same post pasted as a permalink and a redd.it link, or a comment permalink inside a post you also pasted whole — and every one is dropped and logged, never billed.


When a run fails

This Actor ends SUCCEEDED with a status message, not FAILED, on everything transient:

  • 0 rows → status message names the cause (posts not found / Reddit did not return the tree / filters removed everything / no usable URLs) and the log shows per-post detail.
  • Bad URL → skipped with a warning naming the line; the other posts still run.
  • Challenge did not clear → the session is re-minted once on a fresh exit IP; if that also fails the run stops there (remaining post URLs are not attempted) and ends SUCCEEDED with a status message naming the cause. A challenge failure mid-post re-mints and retries that post once, then skips it and continues.
  • Time budget → stops cleanly with every fetched post pushed and a status message; never TIMED-OUT.

It ends the run FAILED in exactly one place: if the post-run uniqueness assertion fails (a duplicate would have been billed). That has never fired.


Who buys this

  • Voice-of-customer and brand teams reading what a thread says about a product — the tree fields let you weigh a top-level complaint by the argument it started.
  • AI / LLM teams building conversation datasets: thread_path + depth + position is a ready-made dialogue structure, is_deleted and score are the filters.
  • Market researchers and analysts quoting Reddit sentiment with provenance (permalink, created_iso, score, score_hidden).
  • Community and PR monitors re-pulling the same thread on a schedule and diffing comments_delivered_for_post.
  • Agents and automations that get handed a Reddit link and need the discussion back as rows — every URL shape Reddit produces is accepted.

FAQ

Does this need an account, a login or cookies? No. It opens a logged-out browser session, clears Reddit's public challenge, and reads public JSON. No credentials, no cookie jar, no CAPTCHA service.

Can I get the whole thread, however big? Yes — maxCommentsPerPost: 0. The 1,359-row thread took 92 calls; a 10,000-comment thread is several hundred calls across a couple of sessions — set the run timeout accordingly.

Why does the row count not match post_num_comments? Reddit's counter is kept separately from the tree: it can be under (it drops [deleted] placeholders the tree still serves — 1,323 counted vs 1,359 delivered) or over (ids removed by spam filters that no endpoint serves — comments_absent_at_source). tree_complete: true tells you every stub was expanded and every id was asked for.

Do I need a residential proxy? Yes. Measured: Chromium on datacenter 0/3, on residential-US 10/10. The default is already correct.

Will two runs give me duplicates? Within a run, never — asserted. Across runs, the name field is Reddit's permanent id, so dedupe on it.

Is this legal? The content is public, and it is also user-generated text with usernames in it — see below.


This Actor reads public, logged-out pages and public JSON endpoints on www.reddit.com. It does not log in, does not use any Reddit account or cookie, and collects nothing behind authentication. Reddit's robots.txt for www.reddit.com and old.reddit.com reads User-agent: * / Disallow: / and references Reddit's Public Content Policy; Reddit's User Agreement and Public Content Policy restrict commercial use of its content. You are responsible for complying with Reddit's terms and with how you use the data, including data-protection law — usernames and comment text can be personal data.

Reddit® is a trademark of its owner. This Actor is not affiliated with, endorsed by, or connected to Reddit, Inc.


Feedback

Found a URL shape that does not parse or a field you need? Open an issue on the Issues tab, and if the Actor earns it, a review on the Reviews tab helps other buyers find it.