# Reddit Posts & Comments Scraper - Subreddits & Search (`eiv/reddit-posts-comments-scraper`) Actor

Reddit scraper for subreddit feeds, keyword search, user submissions and full comment threads. Returns score, upvote ratio, comment count, flair, awards and media URLs per post, with nested comment trees. No login, no API key, no rate-limit quota.

- **URL**: https://apify.com/eiv/reddit-posts-comments-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.90 / 1,000 post scrapes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Posts & Comments Scraper

Scrape Reddit posts and their comment threads from **subreddits, keyword searches, user profiles and individual post links** — no Reddit account, no API key, and none of the rate-limit quota the official API imposes.

One row per post, one row per comment, and a summary row per source that tells you exactly what was and was not collected.

***

### What you get on every post

| | |
|---|---|
| **Engagement** | score, upvote ratio, comment count |
| **Content** | title, full self-text, post kind (text / link / image / gallery / video / poll) |
| **Links & media** | destination URL, domain, and direct media URLs — the original file, not a resized preview |
| **Context** | subreddit, subscriber count, flair, NSFW, spoiler, pinned, locked, archived |
| **People** | author username plus Reddit's stable account id, which survives a rename |
| **Timing** | UTC timestamps for posting and for the last edit |
| **Moderation** | who removed it, and whether the author posted with a mod or admin badge |

**Upvote ratio** is the field most Reddit exports leave out, and it is the one that separates a post everyone agreed with from a post that was fought over. Both can score 500.

### What you get on every comment

The comment text, author, score, nesting depth, its parent comment id, a direct permalink, and whether it was written by the person who made the post. Every comment row also carries the **post id and post title**, so it reads on its own without joining anything back together.

Comments come back **depth-first**, so a parent always appears before its replies and the thread reconstructs in the order it is written.

***

### Four ways to point it at Reddit — mix them freely in one run

**Subreddits** — `programming`, `r/programming`, or the full URL. Sort by hot, new, top, rising or controversial, over any timeframe from the past hour to all time.

**Keyword search** — across all of Reddit, or restricted to subreddits you name. Reddit's own search operators work, including quoted phrases, `OR`, and `flair:"name"`.

**Users** — everything a given account has posted.

**Individual posts** — paste any post permalink or `redd.it` short link. A post URL always returns its comment thread, because Reddit serves a post and its comments together.

Give it a subreddit twice — once as a URL, once as a bare name — and it is scraped once, not twice.

***

### Filters that cut the bill, not just the spreadsheet

Filtered posts are **never written and never charged**. They are not rows you pay for and then delete.

- **Minimum score** and **minimum comment count** — reach versus discussion
- **Post kind** — only text posts, only outbound links, only images and galleries, or only videos
- **Skip pinned posts** — drops the moderator announcements that sit on top of every page
- **Exclude NSFW**

`minComments` is the one to reach for before turning comments on: it spends the extra requests only where there is a discussion worth reading.

***

### An empty result always tells you why

Here is a thing about Reddit that quietly breaks scrapers. Ask it for a subreddit that **does not exist** and it does not return an error — it returns HTTP 200 and an empty feed, byte-for-byte identical to what a real but quiet subreddit returns. A typo in your input list and a genuinely dead community are indistinguishable.

So when a source returns no posts, this Actor spends **one extra request** checking that source against its own record on Reddit, and tells you which it was:

| `zeroReason` | What it means |
|---|---|
| `listing-empty-confirmed` | The subreddit or user really does exist, and really has nothing matching |
| `subreddit-empty-confirmed` | It exists and has no subscribers at all |
| `subreddit-quarantined` | It exists, but Reddit gates its posts behind an opted-in account |
| `search-matched-nothing` | The search ran and matched nothing — and if it was scoped, the scope subreddit was verified to exist |
| `all-posts-filtered-out` | Posts were found; **your filters** removed all of them |
| `run-timeout-reached` | The run stopped itself before the platform could kill it |

And if the check comes back saying the subreddit or user is not there, you get a typed `not-found` error row naming it — not a summary row implying it was empty. A **suspended** Reddit account is reported as such rather than as a user with no posts.

That extra request costs nothing on a normal run, because it only happens for a source that produced nothing.

Every source also gets a **summary row** carrying how many posts were examined, how many the filters dropped, how many pages were read, and why it stopped.

Sources that could not be read at all get a typed **error row** instead, and error rows are never charged. Reddit's four refusals are told apart rather than lumped together:

| `errorClass` | What actually happened |
|---|---|
| `not-found` | No such subreddit, user or post |
| `banned` | Reddit banned the subreddit |
| `private` | The subreddit exists but is closed |
| `quarantined` | Needs an opted-in Reddit account |
| `blocked` / `rate-limited` | A transport problem, retried on a fresh IP before it was reported |

***

### Honest about limits, because Reddit has them

- **About 1,000 posts per listing.** Reddit stops paginating any single subreddit feed, search or user profile there, whatever is requested. More than that has to come from more sources — a narrower timeframe, a tighter search, a different sort — and this Actor will not pretend otherwise.
- **Very large comment threads are partial.** Reddit withholds the tail of a huge thread behind "load more" stubs. Those stubs carry counts but no text, so they are never counted as comments or billed. The post row is marked `commentsTruncated: true` instead, so you can see it rather than discover it later.
- **Scores are approximate by design.** Reddit fuzzes vote counts to frustrate manipulation. Treat a score threshold as a guide.
- **Awards are gone, and this Actor will not pretend otherwise.** Reddit retired awards and coins in 2023, so `totalAwards` reads 0 on every post — including years-old ones. The field is passed through because Reddit still sends it, and it is listed here rather than advertised as a feature, because it no longer carries any information.
- **Sorts differ per source.** Reddit accepts `relevance` on a subreddit feed, ignores it, and quietly answers with `hot`. Rather than pass that through, an unsupported sort is swapped for the closest supported one and the change is written to the log.
- **Comments are slow, and the clock is real.** Reddit hands over 100 posts in a single request, but a comment thread is one request *per post*, each taking 20–40 seconds through the unblocking proxy. Budget roughly one post per second per worker with comments on, against a hundred per request without. Set the run timeout accordingly — and if the run does start running out of time, it **stops itself cleanly** and marks every summary row `runTimeout` rather than being killed halfway with nothing saved. Posts and their comments are written as each post finishes, so a run you stop early keeps everything it had already collected.

***

### Pricing

Pay per result. You are charged for rows that were actually written.

| Event | Price |
|---|---|
| **Post scraped** | **$1.50 per 1,000 posts** |
| Comment scraped | $0.50 per 1,000 comments |
| Comment thread opened | $2.50 per 1,000 posts, only when comments are on |
| Actor start | $0.00005 per run |

Prices fall on every paid Apify plan, to **$0.90 per 1,000 posts** at the top tier — still a fraction of what the established Reddit scrapers charge at their own volume floor.

Summary rows are free. Error rows are free. Filtered posts are free. Posts Reddit repeated across pages are deduplicated by id and charged once. A request that came back challenged and was retried is not charged.

**A posts-only run is the cheap one.** Reddit returns 100 posts per request, so scraping 1,000 posts costs $1.50 and takes a handful of requests. Turning comments on costs one extra request per post, which is why opening a thread is priced separately — including for a post that turns out to have no comments at all, since that answer cost a real request too.

***

### Getting started

Put a subreddit in **Subreddits**, press **Start**, and look at the results. The defaults collect 100 posts per source with comments off.

Then, when you know what you want:

- Turn on **Also scrape comments** and set **Minimum comment count** so the extra requests go where the discussion is
- Set **Sort** to `top` and **Timeframe** to `year` for a subreddit's greatest hits
- Use **Search keywords** with **Search inside these subreddits** to track a brand, product or competitor across the communities that matter
- Raise **Max posts in the run** once you have seen the shape of the data

Export to JSON, CSV, Excel or XML, or pull the dataset straight from the Apify API.

# Actor input Schema

## `subreddits` (type: `array`):

Subreddits to read, one per line. Accepts a bare name (programming), the r/ shorthand (r/programming), or a full URL. Reddit serves at most about 1,000 posts per subreddit feed, so to go deeper use a narrower sort or timeframe rather than a bigger limit.

## `searchQueries` (type: `array`):

Keyword searches to run, one per line. Reddit's own search operators work here, including quoted phrases, OR, and flair:"name". Leave Search inside subreddits empty to search all of Reddit.

## `searchInSubreddits` (type: `array`):

Restrict every search keyword to these subreddits, one per line. Accepts a bare name, the r/ shorthand, or a full URL. Reddit cannot restrict one search to several subreddits at once, so each keyword and subreddit pair is run as its own search. Ignored when Search keywords is empty. If you fill this in and none of the names can be read, the searches are NOT run against all of Reddit instead - you get a bad-input row per unreadable name, because widening a scoped search silently would return the wrong data at full price.

## `usernames` (type: `array`):

Users whose submitted posts you want, one per line. Accepts spez, u/spez or a profile URL. Returns posts only; a user's comments are not part of this listing.

## `startUrls` (type: `array`):

Any mix of Reddit URLs: subreddit pages, user profiles, search result pages with a ?q= term, and individual post permalinks (including redd.it short links). A post URL always returns its comment thread, because Reddit serves a post and its comments in a single response.

## `sort` (type: `string`):

How Reddit should rank the feed. Reddit supports a different set per source type and silently ignores the rest, so an unsupported choice is swapped for the closest one and reported in the log: subreddits accept hot, new, top, rising and controversial; users accept hot, new, top and controversial; searches accept relevance, hot, top, new and most comments.

## `timeframe` (type: `string`):

The window Top and Controversial rank over, and the window every search is limited to. Ignored by Hot, New and Rising, which have no time window.

## `postType` (type: `string`):

Keep only text posts, only outbound links, only images, or only videos. Image also keeps multi-image galleries. Filtered posts are never written and never charged.

## `minScore` (type: `integer`):

Skip posts scoring below this. Reddit fuzzes scores slightly to frustrate vote manipulation, so treat a threshold as approximate. Negative values are allowed.

## `minComments` (type: `integer`):

Skip posts with fewer comments than this. Useful when you want discussion rather than reach, and it cuts the cost of a comments run sharply.

## `skipStickied` (type: `boolean`):

Drop the moderator posts pinned to the top of a subreddit. They appear on every page of a feed and are almost never the data you want.

## `includeNsfw` (type: `boolean`):

Keep posts Reddit marks over-18. Turn this off to drop them.

## `includeComments` (type: `boolean`):

Fetch the comment thread for every post that passes the filters. This costs one extra request per post, so a run with comments takes far longer than the same run without them: budget roughly one post per second per worker instead of a hundred. Use Minimum comment count to spend that time only where there is a discussion to read.

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

Upper bound on comment rows per post, counted depth-first so a parent always comes before its replies. 500 is Reddit's ceiling, not ours: its comments endpoint returns at most 500 nodes per request, so a larger number here could never be reached. Reddit also withholds the tail of a very large thread behind 'load more' stubs; when that happens the post row is marked commentsTruncated true rather than pretending the thread was complete.

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

How Reddit should order the comment tree. Best is Reddit's own default ranking.

## `maxPostsPerSource` (type: `integer`):

Output cap per subreddit, search or user. Reddit stops paginating a single listing at about 1,000 posts however many pages are requested, so 1,000 is the ceiling here too.

## `maxTotalPosts` (type: `integer`):

Hard cap across every source, so a long list of subreddits cannot run away with the bill. Reached first, it stops the run and every remaining source gets a summary row saying so.

## `maxPagesPerSource` (type: `integer`):

Work cap: pages read per source, at 100 posts a page. Ten pages covers the whole reachable feed. The extra headroom is for a shifting listing that repeats posts across pages, which Hot constantly does, since repeats are discarded and do not count toward the post cap.

## `maxConcurrency` (type: `integer`):

How many requests may be in flight at once, across the whole run rather than per source.

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

Minimum gap between the start of one request and the next, applied globally rather than per worker, so this really is the run's request rate. Raise it if the log reports HTTP 429.

## `proxyConfig` (type: `object`):

Leave this alone unless you know otherwise. Reddit refuses plain datacenter and residential IPs alike on these endpoints - measured, both returned a byte-identical HTTP 403 - so the Actor defaults to the Apify Unblocker group, which is the only one that gets through. Unblocker bills per request rather than per gigabyte, and one request carries up to 100 posts.

## Actor input object example

```json
{
  "subreddits": [
    "programming"
  ],
  "sort": "hot",
  "timeframe": "all",
  "postType": "any",
  "minScore": 0,
  "minComments": 0,
  "skipStickied": false,
  "includeNsfw": true,
  "includeComments": false,
  "maxCommentsPerPost": 50,
  "commentSort": "confidence",
  "maxPostsPerSource": 100,
  "maxTotalPosts": 1000,
  "maxPagesPerSource": 10,
  "maxConcurrency": 5,
  "requestDelayMs": 1000,
  "proxyConfig": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per post with score, upvote ratio, comment count, flair, awards and media URLs, plus one row per comment with its author, score and position in the thread. Every source also gets a summary row saying how many posts were read, how many were filtered out, and why the run stopped.

# 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 = {
    "subreddits": [
        "programming"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/reddit-posts-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 = { "subreddits": ["programming"] }

# Run the Actor and wait for it to finish
run = client.actor("eiv/reddit-posts-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 '{
  "subreddits": [
    "programming"
  ]
}' |
apify call eiv/reddit-posts-comments-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eiv/reddit-posts-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/iE3TbAgb7Mcmwi4jz/builds/Pc51TEPfvh1kob4pS/openapi.json
