Reddit Scraper: Subreddit Posts, Comments, Search - No Login
Pricing
Pay per event
Reddit Scraper: Subreddit Posts, Comments, Search - No Login
Extract Reddit posts, comments and search results at scale. Up to 1,000 posts per subreddit across hot/new/top/rising, full comment threads, and keyword search. Flat JSON, 33 always-present fields, agent-ready. No login or credentials. SFW only. No start fee, $0.60 per 1,000 posts.
Pricing
Pay per event
Rating
5.0
(4)
Developer
Santhej Kallada
Maintained by CommunityActor stats
0
Bookmarked
4
Total users
4
Monthly active users
3 days ago
Last modified
Categories
Share
Reddit Scraper: Subreddit Posts, Comments, Search — No Login
Extract posts, full comment threads and keyword search results from public Reddit communities and get back one flat dataset with 33 keys on every row. No login, no API keys, no tokens, no OAuth app — pass a community name and get data.
There is no start fee. You pay for rows, and only for rows that were actually written to your dataset. A run that returns nothing costs $0.00.
- $1.00 per 1,000 posts (search results bill on the same event)
- $0.30 per 1,000 comments
- $0.002 per comment thread that actually returned a comment
- $0.00 to start a run
Why the missing start fee matters
Most scrapers in this category charge a flat fee the moment a run begins, before a single row exists — and on this platform that fee scales with the memory a run reserves, so an Actor that reserves several gigabytes pays it several times over. For an agent making small, frequent calls, that fixed fee is the bill.
A 25-post call, end to end:
| This Actor | Elsewhere in the category | |
|---|---|---|
| Start fee | $0.00 | $0.02 – $0.09 before any data is returned |
| 25 posts | $0.025 | plus $0.0125 – $0.125 |
| Total | $0.025 | $0.033 – $0.215 |
| A run that returns 0 rows | $0.00 | the start fee, every time |
That is 1.3× to 8.6× cheaper on exactly the small, repeated calls agents make all day, and the gap is widest where it hurts most: retries and empty results are free here.
Unnamed price bands across the category (no developer is named — compare for yourself in the Store):
| Axis | This Actor | Published band elsewhere |
|---|---|---|
| Start fee per run | $0.00 | $0.02 – $0.09 |
| Per 1,000 posts | $1.00 | $0.50 – $4.99 |
| Per 1,000 comments | $0.30 | typically the same rate as posts |
| Billed for promoted/duplicate rows | No | commonly yes |
| Billed when a run returns nothing | No | start fee applies |
At very large volumes the per-post rate here sits at the low end of the published band rather than far below it; the decisive advantage is on small and mid-sized calls, and on the fact that failures and empty results never appear on your invoice.
What you get
- Community listings — up to 1,000 posts per community, across
hot,new,top,rising,controversialandbest, with an optional time window. - Comment threads — up to 500 comments per post, with nesting depth, parent links and author flair.
- Keyword search — site-wide or scoped to specific communities, up to ~239 results per keyword.
- One flat dataset — posts and comments in the same table, distinguished by
typeandsourceType, so a multi-step research task is one call instead of three Actors stitched together. - Deterministic shape — all 33 keys present on every row. Inapplicable values are
null, never a missing key, so a parser written once never breaks.
Published ceilings (enforced in the input schema, not discovered at runtime)
| Surface | Hard ceiling |
|---|---|
| Posts per community listing | 1,000 |
| Results per search keyword | ~239 (measured 100 + 100 + 39, then the listing ends) |
| Comments per post | 500 |
Asking for more does not fail — it returns the ceiling. maxCommentsPerPost is an upper bound, not a
guarantee: a request for 50 comments commonly returns 48–49 on deep threads, and very large threads are
sampled rather than exhausted (a 7,000-comment thread returns 500 across roughly 6 levels of nesting). This
is stated up front so a planning agent never has to run a trial job.
SFW communities only — by design
Adult and quarantined communities are refused, not filtered afterwards:
- Every requested community is checked before any harvesting starts. Adult-gated, quarantined, banned and non-existent communities are rejected with a clear input-validation error and cost nothing.
- The primary data path is the one that honours Reddit's own age gate, so the default route cannot return adult content.
- Any individual post marked adult is dropped before it is written and before it is billed — even inside a SFW community.
- Search never opts into adult results, and comment threads are only requested for posts that already passed.
There is no toggle to switch this off. This is a deliberate safety property of the product.
Use cases
- Market and product research — track what a community says about a product, a release or a competitor.
- Trend and topic monitoring — schedule a daily
newpull on a set of communities and diff it. - Sentiment and NLP corpora — post bodies plus full comment threads, already flat and deduplicated.
- Support and bug triage — find complaint threads about your product across relevant communities.
- Lead and community discovery — find the communities where a topic is actually discussed.
- Agent workflows — a small, cheap, predictable call an LLM agent can make dozens of times per task.
Input
Nine fields, eight of them optional. Supply communities, a search keyword, or both — a run with neither fails immediately at validation and costs nothing.
| Field | Type | Default | Notes |
|---|---|---|---|
subreddits | array | ["programming"] | Accepts programming, r/programming, /r/programming or a full reddit.com URL. |
searchQuery | string | — | Site-wide on its own. With subreddits, runs once per community in addition to each community's listing — see below. |
sort | string | hot | hot, new, top, rising, controversial, best. |
timeRange | string | all | hour, day, week, month, year, all. Applies to top, controversial and search. |
maxPostsPerSource | integer | 100 | Max 1,000. Per source, not per run — see below. |
includeComments | boolean | false | Appends comment rows to the same dataset. |
maxCommentsPerPost | integer | 50 | Max 500. Upper bound, not a guarantee. |
minCommentsToFetch | integer | 1 | Skip threads on posts declaring fewer comments than this. Your direct lever on the per-thread charge. |
proxyConfiguration | object | Residential US | Advanced. |
What counts as a source
maxPostsPerSource is a ceiling per source, and a run can have more sources than it has
communities. A source is one community listing, one keyword search inside one community, or one
site-wide search. So the row count — and the bill — is:
| Input | Sources | Rows at maxPostsPerSource: 8 |
|---|---|---|
| 1 community | 1 listing | 8 |
| keyword only | 1 site-wide search | 8 |
| 1 community + keyword | 1 listing + 1 search | 16 |
| 2 communities + keyword | 2 listings + 2 searches | 32 |
Supplying communities and a keyword therefore returns roughly twice what either does alone: you
get each community's current listing and that community's matches for your keyword. If you only
want keyword matches, leave subreddits empty and let the search run site-wide, or pass the keyword
alone and filter afterwards. Every row states its origin in sourceType (subreddit or search)
and sourceQuery, so the two are always separable, and overlap between a listing and a search of the
same community is de-duplicated before billing.
Minimal input
{"subreddits": ["programming"]}
Returns the 100 hot posts of r/programming for $0.10.
Full input
{"subreddits": ["programming", "r/rust", "https://www.reddit.com/r/webdev/"],"searchQuery": "state management","sort": "top","timeRange": "month","maxPostsPerSource": 250,"includeComments": true,"maxCommentsPerPost": 100,"minCommentsToFetch": 5,"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"],"apifyProxyCountry": "US"}}
Search only (site-wide)
{"searchQuery": "rust async runtime","sort": "new","timeRange": "year"}
Output
One dataset. One record shape. Every key present on every row.
Post row
{"type": "post","id": "t3_1abc234","postId": "t3_1abc234","parentId": null,"depth": null,"subreddit": "programming","subredditPrefixed": "r/programming","subredditId": "t5_2fwo","author": "example_user","authorId": "t2_9xk21f","title": "A deep dive into structured concurrency","body": null,"url": "https://example.com/structured-concurrency","permalink": "https://www.reddit.com/r/programming/comments/1abc234/a_deep_dive_into_structured_concurrency/","domain": "example.com","postType": "link","score": 1284,"upvoteRatio": null,"numComments": 213,"numCrossposts": 2,"awardCount": 0,"flair": "Discussion","isNsfw": false,"isSpoiler": false,"isOriginalContent": false,"isPinned": false,"createdAt": "2026-08-14T14:25:48.264Z","createdTimestamp": 1786717548264,"rank": 1,"sourceType": "subreddit","sourceQuery": "programming","sortUsed": "hot","scrapedAt": "2026-08-14T16:41:30.117Z"}
Comment row
{"type": "comment","id": "t1_kx99f0a","postId": "t3_1abc234","parentId": null,"depth": 0,"subreddit": "programming","subredditPrefixed": "r/programming","subredditId": "t5_2fwo","author": "another_user","authorId": "t2_4b7zq2","title": null,"body": "The cancellation semantics are the part people underestimate.","url": null,"permalink": "https://www.reddit.com/r/programming/comments/1abc234/a_deep_dive_into_structured_concurrency/kx99f0a/","domain": null,"postType": null,"score": 42,"upvoteRatio": null,"numComments": null,"numCrossposts": null,"awardCount": 0,"flair": null,"isNsfw": null,"isSpoiler": null,"isOriginalContent": null,"isPinned": null,"createdAt": "2026-08-14T15:58:02.000Z","createdTimestamp": 1786723082000,"rank": 1,"sourceType": "comments","sourceQuery": "programming","sortUsed": "hot","scrapedAt": "2026-08-14T16:41:30.117Z"}
Field reference
| # | Field | Type | Description |
|---|---|---|---|
| 1 | type | string | post or comment. |
| 2 | id | string | t3_ for posts, t1_ for comments. Primary dedupe key. |
| 3 | postId | string | t3_ id of the parent post; equals id on post rows. |
| 4 | parentId | string | Direct parent id. null on posts and top-level comments. |
| 5 | depth | integer | Comment nesting depth, 0 = top level. null on posts. |
| 6 | subreddit | string | Bare community name. |
| 7 | subredditPrefixed | string | r/-prefixed name. |
| 8 | subredditId | string | t5_ id. |
| 9 | author | string | Username without u/. [deleted] is preserved verbatim. |
| 10 | authorId | string | t2_ id. |
| 11 | title | string | Post title, HTML entities decoded. null on comments. |
| 12 | body | string | Self-text, comment text, or search snippet. |
| 13 | url | string | Outbound/media URL. Equals the permalink for text posts. |
| 14 | permalink | string | Absolute link to the post or the specific comment. |
| 15 | domain | string | Link host, or self.{community} for text posts. |
| 16 | postType | string | link, text, image, video, gallery, crosspost. |
| 17 | score | integer | Net upvotes at extraction time. null when hidden. |
| 18 | upvoteRatio | number | 0–1. See the availability matrix below. |
| 19 | numComments | integer | Declared comment count on the post. |
| 20 | numCrossposts | integer | Crosspost count. |
| 21 | awardCount | integer | Award count. |
| 22 | flair | string | Post flair, or user flair on comment rows. |
| 23 | isNsfw | boolean | Always false on delivered post rows — adult rows are never written. |
| 24 | isSpoiler | boolean | Spoiler marker. |
| 25 | isOriginalContent | boolean | OC marker. |
| 26 | isPinned | boolean | Pinned/stickied in the community. |
| 27 | createdAt | string | ISO 8601 UTC. |
| 28 | createdTimestamp | integer | Epoch milliseconds, derived from createdAt so the two can never disagree. |
| 29 | rank | integer | 1-based position within its own listing, search or thread. |
| 30 | sourceType | string | subreddit, search or comments. |
| 31 | sourceQuery | string | The community or keyword that produced the row. |
| 32 | sortUsed | string | The sort actually applied, echoed back. |
| 33 | scrapedAt | string | ISO 8601 UTC extraction time, identical for every row in a run. |
Field availability matrix
The record shape is identical everywhere; the populated set is not, and pretending otherwise would silently break anything filtering on a field it cannot tell apart from unknown. So it is published — and it is published per route, because every one of the three surfaces has a primary route and a fallback route, and the fallback route reports strictly less. The fallback is only used when the primary route is unavailable for that source.
+ populated · – always null · 0 always false by construction
| Field | Listing (primary) | Listing (fallback) | Search (primary) | Search (fallback) | Comments (primary) | Comments (fallback) |
|---|---|---|---|---|---|---|
type id postId permalink rank sourceType sourceQuery sortUsed scrapedAt | + | + | + | + | + | + |
parentId depth | – | – | – | – | + | + |
subreddit subredditPrefixed author score createdAt createdTimestamp | + | + | + | + | + | + |
title | + | + | + | + | – | – |
subredditId | + | + | – | – | + | – |
authorId | + | + | + | + | + | – |
body | + | + | + (snippet) | + (snippet) | + | + |
url domain | + | + | + | – | – | – |
postType | + | + | – | – | – | – |
upvoteRatio | – | + | – | – | – | – |
numComments | + | + | + | + | – | – |
numCrossposts | + | – | – | – | – | – |
awardCount | + | + | – | – | + | + |
flair | + | + | + | – | + (user flair) | – |
isNsfw | 0 | – | – | – | – | – |
isSpoiler isOriginalContent isPinned | + | – | – | – | – | – |
Exactly what a fallback route costs you, so it is never discovered as a surprise null column:
| Surface | Fields the fallback route cannot report that the primary one can |
|---|---|
| Community listing | numCrossposts, isNsfw, isSpoiler, isOriginalContent, isPinned (it gains upvoteRatio) |
| Search | url, domain, flair |
| Comments | subredditId, authorId, flair |
The primary listing route populates 32 of 33 fields (only upvoteRatio is unavailable there); the fallback
listing route populates 26 of 33. If you need upvoteRatio on every row, this Actor is not the right tool —
that is said here rather than discovered as a null column.
Which route you actually get
The primary route is the route — the fallback is not a route most runs touch. Measured on hosted runs against the shipped residential configuration: runs complete on the primary route, at 100 posts per request, and reach the fallback only when the site refuses several consecutive exit addresses in a row for one source.
This is worth stating plainly because it is where the field matrix above becomes a promise rather than a table. Reddit refuses a share of anonymous requests per exit address — it is a property of the address, not of your query, your community or your sort — so the Actor simply takes another address and asks again, up to six times per source, before it gives up on the route. That is the whole mechanism, and it is why the primary route's 32 of 33 fields is the set you should plan against.
Two consequences you can rely on:
- A refused address is never delivered as an empty result. A page carrying no rows is only accepted as "this source is empty" when it is demonstrably a page the surface itself served — an empty community renders its listing container, a refusal does not. Anything else advances the route, and if no route can answer the run fails, with a message naming the source. It never reports a short or empty dataset as a complete one.
- A genuinely empty source is still a normal success. A community with no posts, or a keyword with no
matches, returns zero rows, costs $0.00, and is counted in the run summary's
emptySources— which is what separates it from a failure.
Search rows carry the post's own link target (url, and domain derived from it — self.{community} for a
text post), which is more than a search row usually returns; postType is not among them, because this
surface does not state the post's shape and it is not guessed. body on a search row is the snippet, not
the post text, and sourceType: "search" marks every such row so a parser can branch on it.
Pricing
Pay per event. No start fee.
| Event | Price | When it fires |
|---|---|---|
| Post scraped | $0.001 ($1.00 / 1,000) | Per post row written to the dataset. Search results bill on this same event. |
| Comment scraped | $0.0003 ($0.30 / 1,000) | Per comment row written to the dataset. |
| Comment thread fetched | $0.002 | Once per post whose thread returned at least one comment. Empty and deleted threads are free. |
Cost formula you can compute before calling:
total = 0.001 * posts + 0.002 * threadsFetched + 0.0003 * comments
Worked examples
| Job | Cost |
|---|---|
| 25 posts (typical agent call) | $0.025 |
| 100 hot posts from one community | $0.10 |
| 1,000 posts from one community | $1.00 |
| 3 communities × 500 posts | $1.50 |
| 100 posts + threads (100 threads, 25 comments each) | $1.05 |
| 1,000 posts + threads (700 threads, 8 comments each) | $4.08 |
| A run that returns no rows | $0.00 |
Billing rules
- Charges fire only after rows are verified and successfully written. A failed or empty run is free.
- Promoted rows and cross-source duplicates are dropped before they are written, so you are never billed for them. The count you are charged equals the count of usable rows.
minCommentsToFetchis a direct lever on the per-thread charge — raise it to skip low-value threads.maxPostsPerSourceandmaxCommentsPerPostbound the row events, so your maximum bill is knowable in advance.
Notes on reliability
The Actor manages its own request rate and retries transient failures. Known silent-failure modes are turned into loud errors: if a source cannot be delivered, the run fails with a message you can act on instead of returning a short dataset you would treat as complete. Runs are capped by the ceilings above so a request can never quietly under-deliver against an optimistic limit.
What "loud" means here, concretely. These are the rules, not aspirations:
| Situation | What you get |
|---|---|
| A source really has nothing in it | SUCCEEDED, zero rows, emptySources incremented, $0.00 |
| A page comes back that is not the surface at all | the route is retried on a fresh exit address, then advanced |
| No route can answer a source | that source is reported in failedSources and partial: true |
| Some sources collected, others failed | SUCCEEDED with partial: true — read it before treating the dataset as complete |
| Nothing collected at all, and no source was empty | the run fails — status FAILED, non-zero exit, a message naming the cause, and the summary still written to OUTPUT |
A run that fails reports FAILED. It does not report success with an empty dataset, and it does not charge
you — billing fires only after rows are written.
The run summary is written to the key-value store as OUTPUT and includes posts, rows, tierUsed per
surface, failedSources, emptySources, partial, challengesEncountered, requiredFieldFill per route,
and estimatedChargeUsd. Read partial and failedSources before treating a dataset as complete — they are
there so you never have to infer completeness from a row count.
Run memory: 2 GB, fixed
This Actor runs at 2048 MB, declared in its own definition (minMemoryMbytes, defaultMemoryMbytes and
maxMemoryMbytes are all 2048), so the figure is not a default you can drift off:
- 2 GB is measured, not padded. The extraction path renders no page: the worst case is ~1.15 GB with the fallback surface active and the primary one still alive. Most Actors in this category ship at 4 GB, which doubles the compute line for the same rows.
- Do not lower it. At 1024 MB a large run is OOM-killed by the platform rather than degraded, and on this platform an OOM kill is a failed run, not a slow one.
- Do not raise it. Nothing here uses the extra memory, and compute is billed per GB-hour, so 4 GB simply doubles that part of your bill.
If you run this Actor from the API or a scheduler and pass explicit run options, leave memoryMbytes unset —
the declared default applies. Setting it anyway is the only way to end up outside the tested configuration.
Data use, privacy and compliance — please read
Output contains personal data (usernames, user ids, authored text, timestamps, community membership).
- You are the data controller. You are responsible for establishing a lawful basis (GDPR Art. 6), providing notice where required (Art. 14), and setting a retention period. This Actor is a processing tool; it does not make those determinations for you.
- No profiling of individuals. This Actor must not be used to build person-level profiles or datasets about identified or identifiable individuals. It has no user-profile surface and will not get one.
- Honour deletions. Content deleted or removed on Reddit after extraction must stop being used. Re-verify before reusing an older dataset.
- Special-category warning (GDPR Art. 9). Membership of a community can itself reveal health status, sexual orientation, religious belief or political opinion. Harvesting such communities may constitute Art. 9 processing that you cannot lawfully perform. The "manifestly made public" exception is assessed per item and cannot be applied wholesale by a bulk collector.
- Adult and quarantined communities are refused, with no opt-in (see above).
- Respect Reddit's terms and the rights of the people whose content you collect. If you are unsure whether your intended use is lawful in your jurisdiction, take advice before running at scale.
FAQ
Do I need a Reddit account, an app, or API keys? No. There is nothing to configure beyond the input fields.
What does a run that returns nothing cost? Nothing. There is no start fee and charges only fire on rows that were written.
I got zero rows and the run says SUCCEEDED. Is that real?
Yes — and it is the only case in which you will ever see it. Zero rows plus SUCCEEDED means the source
genuinely had nothing in it, and the run summary says so explicitly in emptySources. If the Actor could not
collect a source, the run does not succeed: the source appears in failedSources with partial: true, or the
whole run reports FAILED with a message naming the cause. A short or empty dataset is never presented to you
as a complete one.
How many posts can I get from one community?
1,000 per listing. That is a hard platform ceiling, not a limitation of this Actor. To go further, run again
with a different sort — cross-sort overlap is typically 30–60%, and deduplicating on id is trivial since
id is stable.
Why does my search return about 240 results and stop? That is the ceiling for a single keyword. Split a broad topic into several narrower keywords, or scope the search to specific communities and run one call per community.
Why did I get 48 comments when I asked for 50?
maxCommentsPerPost is an upper bound. Deleted, collapsed and deeply nested replies reduce the delivered
count. Very large threads are sampled, not exhausted.
Can I scrape adult or quarantined communities? No. They are refused before any data is fetched, and there is no setting to change that.
Can I get comments without posts?
Comments are always attached to the posts they belong to — set includeComments: true and filter the dataset
on type == "comment". Both live in the same table, joined by postId.
Can I export to CSV or Excel? Yes — the dataset exports to JSON, CSV, XLSX, XML and HTML from the run page or the API, and the flat, fixed 33-key shape means the columns are always the same.
Can an AI agent call this? Yes, that is the design target. Nine input fields, one required decision, published ceilings, a computable cost formula, no pagination state and no cursors to manage. One call in, complete dataset out.
Is the output shape stable?
Yes. All 33 keys appear on every row, in the same order, with null for inapplicable values. New fields, if
any, would be added at the end.
Tags
reddit · reddit scraper · subreddit scraper · reddit comments · reddit search · social media ·
sentiment analysis · market research · nlp dataset · ai agent · mcp · no login