+ Integration Guide
Keep Your PRAW Code: A Drop-In Replacement for Reddit's API
Your PRAW bot, pipeline or cron job can keep reading Reddit after the API closes. Change one config file, not your code. Audit, rollout checklist and costs.
Written by Alex P.
- PRAW drop-in replacement
- keep PRAW working
- PRAW without code changes
- PRAW after March 2027
- praw.ini
- asyncpraw
- PRAW replacement
- reddit API python
Keep your PRAW code. Change one file and one secret.
# your environment or secret store
praw_client_id=ss-your-fetchlayer-key # was your Reddit client ID
praw_client_secret=unused
praw_user_agent=my-app
# ~/.config/praw.ini (%APPDATA%\praw.ini on Windows)
[DEFAULT]
oauth_url=https://praw.fetchlayer.dev
reddit_url=https://praw.fetchlayer.dev
That’s the whole change for code that reads Reddit. Your praw.Reddit() call, your listings, your replace_more() loops and your submission streams stay exactly as they are. You don’t need a Reddit app, a Reddit account or Reddit’s approval.
This page is for teams with PRAW already in production: research pipelines, cron jobs, Discord and Slack bots that read Reddit, data jobs. Reddit closes public API access in March 2027, and new access requests stop on October 31, 2026. Here is how to check your codebase, roll the change out, and know what it will cost. For a first-time setup walkthrough, see Migrate PRAW to FetchLayer.
What stays exactly the same
Every read-only PRAW call returns the same PRAW objects:
| Your code | Works unchanged |
|---|---|
reddit.subreddit("x").hot() / .new() / .top() / .rising() / .controversial() | ✅ including r/all and the front page |
reddit.submission(id=...) / reddit.submission(url=...) | ✅ with .comments |
submission.comments.replace_more(limit=...) and .comments.list() | ✅ |
reddit.subreddit("x").search(...) | ✅ |
reddit.redditor("x").submissions.* / .comments.* (new, hot, top) | ✅ |
Lazy attributes: subreddit.subscribers, redditor.link_karma, comment.body… | ✅ |
reddit.info(fullnames=[...]), reddit.info(subreddits=[...]) | ✅ |
subreddit.stream.submissions(), redditor.stream.submissions() | ✅ (stream.comments() is not supported) |
Async PRAW (asyncpraw.Reddit) | ✅ same praw.ini |
Errors keep PRAW’s exception types too: NotFound for 404s, Forbidden for 403s, and 429s are waited out using Retry-After.
What you change
PRAW reads two kinds of settings from different places, and that decides what you edit:
- Credentials (
client_id,client_secret,user_agent,username,password) come frompraw.ini, the constructor, orpraw_*environment variables, and environment variables overridepraw.ini. - The host (
oauth_url,reddit_url) comes only frompraw.inior the constructor. Environment variables alone can’t change where PRAW sends requests, so every option below puts the URLs in a file or in code.
Option 1: credentials in env vars, a 3-line praw.ini for the host
The split most teams prefer, shown at the top of this page. Set praw_client_id to your FetchLayer key, praw_client_secret to any value and praw_user_agent to anything. Then add a praw.ini with only oauth_url and reddit_url. The file holds no secrets, so it can live in your repo or image, and the key stays in your secret store. No code change.
Option 2: everything in praw.ini
If your credentials already live in praw.ini, edit the file:
[DEFAULT]
+ oauth_url=https://praw.fetchlayer.dev
+ reddit_url=https://praw.fetchlayer.dev
- client_id=your-reddit-client-id
- client_secret=your-reddit-client-secret
+ client_id=ss-your-fetchlayer-key
+ client_secret=unused
If your code calls praw.Reddit("mybot") with a named section, make the same edit in [mybot] instead of [DEFAULT]. No code change.
Option 3: one line in praw.Reddit(...)
If your code passes credentials to the constructor:
reddit = praw.Reddit(
- client_id=os.environ["REDDIT_CLIENT_ID"],
- client_secret=os.environ["REDDIT_CLIENT_SECRET"],
+ client_id=os.environ["FETCHLAYER_API_KEY"],
+ client_secret="unused",
user_agent="my-app",
+ oauth_url="https://praw.fetchlayer.dev",
+ reddit_url="https://praw.fetchlayer.dev",
)
Whichever option you pick, remove username and password if you pass them. They aren’t needed.
Watch for an old
praw_client_id. Becausepraw_*environment variables win overpraw.ini, a deployment that still exportspraw_client_idwith your old Reddit client ID keeps sending it, whatever the file says. Change the variable, not the file.
The endpoint-override warning. A
praw.iniin the working directory that setsoauth_urlorreddit_urlmakes PRAW print a security warning, its guard against a planted config file redirecting your credentials. Avoid it by putting the file in the user config directory (~/.config/praw.ini, or%APPDATA%\praw.inion Windows), or by settingPRAW_ALLOW_ENDPOINT_OVERRIDE=1.
The 2-minute verification script
Run this with the same config your production code uses. It exercises the calls most codebases depend on and prints what each one returned:
# verify_praw.py — run where your app runs
import time
import praw
reddit = praw.Reddit(user_agent="praw-verify") # or praw.Reddit("mybot")
print("endpoint:", reddit.config.oauth_url)
def check(name, fn):
start = time.time()
try:
result = fn()
print(f"OK {name:<28} {result} ({time.time() - start:.1f}s)")
except Exception as e:
print(f"FAIL {name:<28} {type(e).__name__}: {e}")
check("subreddit listing", lambda: len(list(reddit.subreddit("python").hot(limit=25))))
check("subreddit attributes", lambda: reddit.subreddit("python").subscribers)
check("search", lambda: len(list(reddit.subreddit("all").search("asyncio", limit=25))))
def thread():
s = next(reddit.subreddit("AskReddit").top(time_filter="week", limit=1))
s.comments.replace_more(limit=3)
return f"{len(s.comments.list())} comments loaded of {s.num_comments}"
check("submission + replace_more", thread)
check("redditor history", lambda: len(list(reddit.redditor("spez").comments.new(limit=25))))
It costs about 11 credits. If the first line doesn’t print https://praw.fetchlayer.dev, PRAW isn’t reading the file you edited; see the checklist below.
Drop-in checklist for teams
-
Find the
praw.iniPRAW actually reads. PRAW loads, in order, its built-in defaults, thenpraw.iniin the user config directory (%APPDATA%on Windows,$XDG_CONFIG_HOMEif set, otherwise~/.config), thenpraw.iniin the current working directory. Later files win. -
A
praw.iniin the working directory prints a warning when it changes the URLs. That’s PRAW’s guard against a planted config file. Move it to the user config directory, or setPRAW_ALLOW_ENDPOINT_OVERRIDE=1in the environment. -
Docker images: copy
praw.inito$HOME/.config/praw.inifor the user the container runs as (/root/.config/praw.inifor root), or setXDG_CONFIG_HOMEto the directory you mount it in. -
Secrets in CI: use Option 1. Keep the URLs in the committed
praw.ini, and put the key in your secret store aspraw_client_id. -
Several environments: add a section for each and choose one with the
praw_siteenvironment variable. That changes nothing in code that callspraw.Reddit()without a site name:[fetchlayer] oauth_url=https://praw.fetchlayer.dev reddit_url=https://praw.fetchlayer.dev client_secret=unusedThen set
praw_site=fetchlayer(andpraw_client_id=ss-your-fetchlayer-key) in staging first, then production. Rolling back means unsetting one variable. -
No timeout setting needed. PRAW waits 16 seconds for each response, and every response arrives well within that: a big thread’s first fetch carries the top of the tree, and
replace_more()loads the rest in further calls. PRAW’s owntimeoutoption is not passed to its HTTP layer, so it has no effect. On a very slow network, set the environment variablePRAWCORE_TIMEOUT=120instead. -
One host only? If your egress allow-list permits a single API host,
https://api.fetchlayer.dev/v1/praw/works the same. Keep the trailing slash. -
Async PRAW reads the same files and environment variables.
What isn’t supported: check your codebase
The PRAW-compatible API is read-only. These return 403 (prawcore.exceptions.Forbidden):
- posting, replying, editing and deleting;
- voting, saving, hiding and reporting;
- subscribing;
- messages, the inbox and modmail;
- moderation (
.mod, approve, remove, distinguish, sticky, lock, flair); reddit.user.me()and preferences.
Wikis (subreddit.wiki), multireddits and subreddit.stream.comments() aren’t available, and neither are the user overview listing, controversial on user listings, and reddit.info(url=...). Use reddit.submission(url=...) instead of reddit.info(url=...).
Find those calls in your repo before you switch:
grep -rnE --include='*.py' \
'\.(submit(_image|_video|_gallery|_poll)?|reply|edit|delete|upvote|downvote|clear_vote|save|unsave|hide|unhide|report|crosspost|subscribe|unsubscribe|mark_read|mark_unread|send_message|block|unblock|distinguish|sticky|lock|unlock|approve|remove|ignore_reports)\(|\.(mod|modmail|inbox|wiki|multireddit|flair)\b|stream\.comments\(|user\.me\(|\.overview\b' .
It’s a broad pattern, so expect some false positives (list.remove(), dict.save()); review each hit. No hits means your code is read-only and the drop-in covers all of it. If you do have write paths, keep them on your Reddit credentials while those last, and plan them for Reddit’s Developer Platform, which is where Reddit wants posting and moderating apps to live. Read paths can move now.
What it costs
Credits are the same as our REST API. One credit is one page of results:
| PRAW call | Credits |
|---|---|
| A listing, per 25 items | 1 (PRAW’s default limit=100 is 4) |
| A search, per page of results (7 to 18 posts a page) | 1 (25 results is usually 3) |
reddit.submission(...) with its comment tree | 1 |
Each “load more comments” replace_more() expands | 1 |
| Subreddit or redditor attributes | 1 |
reddit.info(fullnames=[...]), up to 100 ids | 1 |
stream.submissions() | 4 credits for the first poll, then 1 credit per poll |
comments.list() | free |
Typical workloads, at pay-as-you-go rates ($1.99 per 1,000 credits; plans are cheaper per credit, see pricing):
| Workload | Credits | Cost |
|---|---|---|
A research script: 1,000 posts from top(limit=1000) | 40 | $0.08 |
…plus each post’s comment tree with replace_more(limit=5) | up to 6,000 | up to $11.94 |
A cron job checking 10 subreddits’ new(limit=25) every 15 minutes | 960 a day | about $57 a month |
A Slack bot that reads one thread with replace_more(limit=10) on demand | up to 11 per thread | about 2¢ |
Two habits keep the bill down: pass the limit you actually need instead of PRAW’s default of 100, and bound replace_more() instead of using limit=None on huge threads.
Frequently Asked Questions
Do I need a Reddit app or a Reddit account?
No. Your FetchLayer key is the only credential, and it works as client_id, client_secret or password. You don’t need username, a Reddit password or a Reddit app.
Is it really a drop-in replacement?
For reading Reddit, yes. Your code gets the same PRAW objects from the same calls. A few fields are sent empty, such as upvote_ratio, awards, and edited on comments. Comments come in Reddit’s default order, so comment_sort isn’t applied. The docs list every difference.
What about rate limits?
Your plan sets the rate. Above it, you get 429 with Retry-After, and PRAW waits automatically. You don’t need the sleep calls you may have added for Reddit’s 100-requests-a-minute limit.
Does Async PRAW work?
Yes, with the same praw.ini or constructor arguments.
What happens to my code if I switch back?
Nothing. Remove the oauth_url and reddit_url lines, or unset praw_site, and PRAW goes back to Reddit’s defaults. The code never changed, so there’s nothing to revert.
Will this still work after March 2027?
Yes. Reddit’s closure ends third-party access to Reddit’s public API. Your PRAW code no longer depends on that access, because it reads through FetchLayer.