+ 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 codeWorks 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 from praw.ini, the constructor, or praw_* environment variables, and environment variables override praw.ini.
  • The host (oauth_url, reddit_url) comes only from praw.ini or 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. Because praw_* environment variables win over praw.ini, a deployment that still exports praw_client_id with your old Reddit client ID keeps sending it, whatever the file says. Change the variable, not the file.

The endpoint-override warning. A praw.ini in the working directory that sets oauth_url or reddit_url makes 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.ini on Windows), or by setting PRAW_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.ini PRAW actually reads. PRAW loads, in order, its built-in defaults, then praw.ini in the user config directory (%APPDATA% on Windows, $XDG_CONFIG_HOME if set, otherwise ~/.config), then praw.ini in the current working directory. Later files win.

  • A praw.ini in 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 set PRAW_ALLOW_ENDPOINT_OVERRIDE=1 in the environment.

  • Docker images: copy praw.ini to $HOME/.config/praw.ini for the user the container runs as (/root/.config/praw.ini for root), or set XDG_CONFIG_HOME to 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 as praw_client_id.

  • Several environments: add a section for each and choose one with the praw_site environment variable. That changes nothing in code that calls praw.Reddit() without a site name:

    [fetchlayer]
    oauth_url=https://praw.fetchlayer.dev
    reddit_url=https://praw.fetchlayer.dev
    client_secret=unused

    Then set praw_site=fetchlayer (and praw_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 own timeout option is not passed to its HTTP layer, so it has no effect. On a very slow network, set the environment variable PRAWCORE_TIMEOUT=120 instead.

  • 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 callCredits
A listing, per 25 items1 (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 tree1
Each “load more comments” replace_more() expands1
Subreddit or redditor attributes1
reddit.info(fullnames=[...]), up to 100 ids1
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):

WorkloadCreditsCost
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,000up to $11.94
A cron job checking 10 subreddits’ new(limit=25) every 15 minutes960 a dayabout $57 a month
A Slack bot that reads one thread with replace_more(limit=10) on demandup to 11 per threadabout 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.