# Reddit Post Comments Scraper — Full Comment Tree, Any Post URL (`scrapersdelight/reddit-post-comments-scraper`) Actor

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.

- **URL**: https://apify.com/scrapersdelight/reddit-post-comments-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Social media, AI, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 per comment returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

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

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.

```json
{
  "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.

| Group | Fields | Example |
|---|---|---|
| **Identity** | `id`, `name`, `post_id`, `permalink`, `subreddit`, `subreddit_id` | `nf7ge1o` · `t1_nf7ge1o` · `1nlqi0c` |
| **Tree** | `parent_id`, `parent_comment_id`, `is_top_level`, `depth`, `position`, `reply_count`, `descendant_count`, `thread_path[]` | depth `2` · 8 replies · 28 descendants · `["nf7d1fx","nf7etqb"]` |
| **Author** | `author`, `author_fullname`, `author_flair_text`, `author_is_op`, `author_premium` | `LividLife5541` · `t2_1l047qectw` |
| **Content** | `body`, `body_length`, `is_deleted`, `edited`, `distinguished`, `stickied`, `locked`, `collapsed`, `collapsed_reason` | 183 chars |
| **Votes** | `score`, `ups`, `score_hidden`, `controversiality`, `total_awards_received`, `gilded` | `394` |
| **Time** | `created_utc`, `created_iso` | `2025-09-20T06:48:41.000Z` |
| **Post context** | `post_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_archived` | `1323` comments · ratio `0.96` |
| **Provenance** | `source` (`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.

| Field | Fill | Notes |
|---|---|---|
| `id` / `name` / `post_id` / `parent_id` / `permalink` | 100% | |
| `body` / `body_length` | 100% | `[deleted]` / `[removed]` placeholders included (see below) |
| `author` | 100% | `[deleted]` for deleted accounts — still a string |
| `score` / `ups` / `controversiality` / `gilded` / `total_awards_received` | 100% | |
| `created_utc` / `created_iso` | 100% | |
| `depth` / `position` / `reply_count` / `descendant_count` | 100% | 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_at` | 100% | |
| `comments_delivered_for_post` | 100% | integer, same value on every row of a post |
| `comments_absent_at_source` | 100% | 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_fullname` | 91.2% | absent on deleted accounts |
| `parent_comment_id` / `thread_path` | 84.5% | by definition null/empty on the 15.5% top-level rows |
| `is_top_level` | 15.5% | a true boolean count, not a gap |
| **`is_deleted`** | **8.8%** | deleted + moderator-removed placeholders |
| **`edited`** | **3.6%** | ISO time when edited, `false` otherwise |
| **`collapsed_reason`** | **3.5%** | e.g. `DELETED`, `LOW_SCORE` |
| **`author_premium`** | **1.6%** | |
| **`collapsed`** | **1.4%** | |
| **`author_flair_text`** | **0.0%** | r/programming does not use user flair; subreddits that do fill it |
| **`author_is_op`** | **0.0%** | the OP did not reply in this thread — a real zero |
| **`distinguished` / `stickied` / `locked` / `score_hidden`** | **0.0%** | no mod comments, nothing pinned, scores settled |
| **`post_selftext_preview` / `post_link_flair_text`** | **0.0%** | a link post has no selftext; self posts fill it (first 500 chars) |
| **`tree_complete`** | **0.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)

```json
{
  "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

```json
{ "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

```json
{ "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

```json
{ "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:

```bash
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.

```jsonc
{
  "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.

| Field | Type | Default | What it does |
|---|---|---|---|
| `postUrls` | string list | 1 sample post | Permalinks, `redd.it`, `/s/` share links, profile-post links, comment permalinks, bare ids. One post pasted twice is fetched once. |
| `commentSort` | select | `top` | `top`, `best`, `new`, `controversial`, `old`, `qa` — decides which rows come first under a cap. |
| **💷 Limits & cost** | | | |
| `maxCommentsPerPost` | integer | `200` | Per post. **0 = the whole tree.** |
| `maxTotalComments` | integer | `0` | Hard cap for the run. 0 = none. The run's *Max total charge* is honoured to the whole row as well. |
| **🌳 Tree depth & filters** | | | |
| `expandMoreComments` | boolean | `true` | Expand "load more" / "continue this thread" stubs. Off = page one only. |
| `maxDepth` | integer | `0` | Keep depth ≤ N (0-based). 0 = unlimited. Filtered rows are never billed. |
| `includeDeleted` | boolean | `true` | Off drops `[deleted]` / `[removed]` placeholders, unbilled. |
| **⚙️ Proxy & performance** | | | |
| `proxyConfiguration` | proxy | Apify RESIDENTIAL, country US | Required — see the transport table. |
| `postsPerSession` | integer | `40` | Posts served per challenge-cleared browser session before a fresh exit IP is minted. |
| `requestDelayMs` | integer | `300` | Pacing 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.

| Run | Comments | Cost |
|---|---|---|
| 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-thread | 10,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](https://apify.com/scrapersdelight/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 `&amp;` 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:**

| Rung | Listing `.json` | Notes |
|---|---|---|
| Raw HTTP, direct | 0 / 12 | 403, Reddit's snooserv JS-challenge block page |
| Raw HTTP, Apify datacenter | 0 / 18 | 403 (www) / "Blocked" page (old.reddit) |
| Raw HTTP, Apify RESIDENTIAL | 0 / 18 | 403 same page; old.reddit serves a login shell with 0 comments |
| Raw HTTP, RESIDENTIAL + country US | 0 / 18 | same |
| Chromium + desktop UA, Apify datacenter | 0 / 3 | challenge never clears |
| **Chromium + desktop UA, RESIDENTIAL + US, in-page fetch** | **10 / 10** | 5 fresh sessions, 2,602 comments, raw `depth` ≤ 9 |

Sustained load on the shipped rung, this build:

| Test | Result |
|---|---|
| Full tree of the "1,323-comment" post, one session | **1,359 rows, 92 calls, 124 s, 0 failed calls, tree\_complete true** |
| 120-second budgeted run, same post, hard-killed at 130 s | exit 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.

***

### Legal & fair use

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.

# Actor input Schema

## `postUrls` (type: `array`):

One or more Reddit posts. Accepts full permalinks (`https://www.reddit.com/r/programming/comments/luq9oz/…/`), `redd.it/luq9oz` short links, mobile share links (`/r/sub/s/AbCdEfG`), profile-post links (`/user/name/comments/…`), bare ids (`luq9oz` or `t3_luq9oz`), and **comment permalinks** — paste a link to one comment and you get just that comment's thread. Old/new/np subdomains all work.

## `commentSort` (type: `string`):

The order Reddit ranks the tree in — this decides which comments come first when a cap applies. `top` = highest score first; `best` = Reddit's default "best" ranking; `new`, `old`, `controversial`, `qa` as on the site.

## `maxCommentsPerPost` (type: `integer`):

Stop after this many comments for each post (nested replies count). **0 = the whole tree**, however deep — a 1,359-comment thread took 92 calls / ~2 min; a 10,000-comment thread is several hundred calls, so give the run time. The shipped default of 200 keeps a first click under $0.40.

## `maxTotalComments` (type: `integer`):

Hard cost cap across all posts. 0 = no cap (the per-post cap still applies). The run's *Max total charge* setting works as well and is honoured to the whole row.

## `expandMoreComments` (type: `boolean`):

Reddit's first page of a big thread hides part of the tree behind "load more comments" and "continue this thread" links (measured: a 1,323-comment post shows 479 on page one, 189 stubs hide the other 844). On, the Actor expands them through Reddit's own `morechildren` and subtree endpoints until the tree is complete or your cap is hit. Off = page one only (fast, cheap, `tree_complete` will be false on big threads).

## `maxDepth` (type: `integer`):

Keep only comments at this nesting depth or shallower (0-based: top-level comments are depth 0). **0 = unlimited.** Set 1 to keep top-level comments and their direct replies (depths 0 and 1) (their `reply_count` / `descendant_count` are still computed from the full tree). Filtered rows are never billed.

## `includeDeleted` (type: `boolean`):

Reddit keeps a placeholder row for deleted and moderator-removed comments so the tree stays intact. On (default) they are delivered with `is_deleted: true`; off, they are dropped and not billed (their replies are still delivered, with `parent_comment_id` pointing at the missing row).

## `proxyConfiguration` (type: `object`):

**Residential proxy (country US) is required.** Measured 2026-08-22 through Apify: Chromium on RESIDENTIAL-US cleared Reddit's browser challenge 10/10 across 5 fresh sessions; datacenter Chromium 0/3; plain HTTP 0/66 on every rung. The default below is already correct — do not switch it to datacenter.

## `postsPerSession` (type: `integer`):

How many posts one challenge-cleared browser session serves before a fresh session (new exit IP) is minted. Each mint costs ~13 s. A failed post also triggers a re-mint.

## `requestDelayMs` (type: `integer`):

Pacing between the JSON calls that expand a tree. Lower is faster but more likely to be throttled. Measured: a 1,359-comment tree = 92 calls, ~2 minutes.

## Actor input object example

```json
{
  "postUrls": [
    "https://www.reddit.com/r/programming/comments/luq9oz/how_i_cut_gta_online_loading_times_by_70/"
  ],
  "commentSort": "top",
  "maxCommentsPerPost": 200,
  "maxTotalComments": 0,
  "expandMoreComments": true,
  "maxDepth": 0,
  "includeDeleted": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "postsPerSession": 40,
  "requestDelayMs": 300
}
```

# Actor output Schema

## `comments` (type: `string`):

Flat comment rows with tree fields, author, score, body, timestamps and post context.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postUrls": [
        "https://www.reddit.com/r/programming/comments/luq9oz/how_i_cut_gta_online_loading_times_by_70/"
    ],
    "commentSort": "top",
    "maxCommentsPerPost": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/reddit-post-comments-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "postUrls": ["https://www.reddit.com/r/programming/comments/luq9oz/how_i_cut_gta_online_loading_times_by_70/"],
    "commentSort": "top",
    "maxCommentsPerPost": 200,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/reddit-post-comments-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postUrls": [
    "https://www.reddit.com/r/programming/comments/luq9oz/how_i_cut_gta_online_loading_times_by_70/"
  ],
  "commentSort": "top",
  "maxCommentsPerPost": 200
}' |
apify call scrapersdelight/reddit-post-comments-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/reddit-post-comments-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/n3RpCkUX5mkpiMcQ9/builds/Fw23qT0CmRc2R1fsl/openapi.json
