How to Scrape Instagram With Python (and Other Languages)
- A plain
requests.getto Instagram'sweb_profile_infoendpoint returned HTTP 429 with a 0-byte body for me in June 2026, and a browser User-Agent, a Googlebot string, and an app-id header all returned the identical 429. The block sits at the TLS layer, below the headers. - Swapping
requestsfor curl_cffi withimpersonate="chrome"returned 200 and 460 KB of profile JSON on the first try: username, follower count, bio, post count, and the publicbusiness_emailfield. - Instaloader is the no-login Python library route for profiles, posts, and comments. It works for a while, then Instagram answers anonymous calls with 401 or 429.
- For volume across many accounts I send one HTTP request to a scraper API that handles the TLS fingerprint, proxies, and retries, then returns parsed JSON. The same shape works from Python, Node.js, or any language that makes an HTTP call.
I tried the lazy way first: one requests.get against Instagram’s profile JSON endpoint from my machine. It came back 429 with an empty body before I had parsed a single field. That failure is the real subject of any honest guide on how to scrape data from Instagram using Python, because it is what almost everyone hits on the first try, and the usual fix you read about (set a browser User-Agent) did nothing for me.
Below is what I ran in June 2026, the exact responses Instagram returned, and the Python that actually pulled data back: curl_cffi matching a browser fingerprint, the Instaloader library for a no-login route, and a scraper API for volume. I tested each method against live accounts like nasa and instagram from my own box, so the status codes and byte counts here are what I saw on screen. The same patterns port to other languages, which I cover at the end.
What are the ways to scrape Instagram with Python?
There are four practical ways to scrape Instagram with Python, and they trade setup effort against how much data and reliability you get. The right one depends on volume, whether you can match a browser fingerprint, and whether the target is public.
| Method | What it pulls | Login needed | Holds up at scale | My note |
|---|---|---|---|---|
requests (the naive call) | Nothing, returns 429 | No | No | Fails at the TLS layer before any data |
curl_cffi (fingerprint match) | Public profile JSON | No | For a while, then 429 | The fix that actually returned data for me |
| Instaloader (library) | Profiles, posts, comments, stories | Optional | For a while, then 401/429 | Best no-login library, MIT-licensed |
| Scraper API | Profiles, followers, comments, more | No | Yes | One HTTP call, fingerprint and proxies handled server side |
The first row is the trap; the next three are the routes worth your time. Two facts shape all of them. Instagram serves public profile data as JSON through web endpoints but refuses clients whose TLS signature or IP looks automated, and the official Instagram Graph API does not expose most of what people want to scrape: Meta shut down the read-only Instagram Basic Display API on December 4, 2024, and its replacement covers only your own Business and Creator accounts, not arbitrary public profiles or follower lists. So scraping the public web surface is the route, and the rest of this guide is the code for doing it; start with why the obvious call fails.
Why does a plain Python request to Instagram fail?
A plain Python request to Instagram fails because Instagram refuses the connection by IP reputation and TLS fingerprint, returning 429 Too Many Requests with an empty body before it serves any data. The block happens below the HTTP headers, so the User-Agent you set never gets a chance to matter.
Here is the naive call I ran first. This is the request that returned 429 for me, so you can recognize it in your own logs:
import requests
url = "https://www.instagram.com/api/v1/users/web_profile_info/?username=nasa"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"x-ig-app-id": "936619743392459", # the public web app id
}
r = requests.get(url, headers=headers, timeout=20)
print(r.status_code) # -> 429
print(r.headers.get("content-type")) # -> text/plain
print(len(r.content)) # -> 0
I tested that same web_profile_info endpoint four ways in June 2026 to find out whether the header was the problem. It was not:
Request through requests | User-Agent sent | Status | Body |
|---|---|---|---|
web_profile_info?username=nasa | full Chrome desktop string | 429 | 0 bytes |
web_profile_info?username=nasa | none | 429 | 0 bytes |
web_profile_info?username=instagram | instagramscraperapi.com/1.0 | 429 | 0 bytes |
web_profile_info?username=instagram | Googlebot string | 429 | 0 bytes |
Four different User-Agents, including no User-Agent and a Googlebot string, returned the identical 429 with a 0-byte body. A 429 with an empty text/plain response and no retry-after value is Instagram refusing the request at the edge, not a normal rate-limit throttle, which would hand back a positive retry-after and some content. The deciding factor is what requests looks like at the TLS handshake: Python’s default HTTPS stack has a fingerprint that Instagram flags before any header is read, which is why changing the User-Agent does nothing. The Scrapfly engineering team, which maintains its own Instagram scraper, documents the same wall in its 2026 Instagram scraping guide: unauthenticated access is capped near 200 requests per hour per IP, and Instagram detects automated clients at the TLS layer, so the fix is to make the request look like a real browser at the handshake, which the next section does.
How do you scrape an Instagram profile with Python?
You scrape an Instagram profile with Python by sending the request through a client that matches a real browser’s TLS fingerprint, which clears the 429 and returns the profile as JSON. The library that does this with the least friction is curl_cffi, a Python binding over curl that can impersonate Chrome’s exact handshake.
Install it:
pip3 install curl_cffi
Then send the same web_profile_info request, with impersonate="chrome" as the only real change from the failing version:
from curl_cffi import requests as creq
url = "https://www.instagram.com/api/v1/users/web_profile_info/?username=nasa"
headers = {"x-ig-app-id": "936619743392459"}
r = creq.get(url, headers=headers, impersonate="chrome", timeout=20)
print(r.status_code) # -> 200
user = r.json()["data"]["user"]
print(user["username"]) # -> nasa
print(user["edge_followed_by"]["count"]) # follower count
print(user["edge_owner_to_timeline_media"]["count"]) # post count
print(user["biography"][:80])
print(user.get("business_email")) # public business email, if set
When I ran this against nasa in June 2026 it returned 200 and a 460,331-byte JSON document, parsed to username nasa, a follower count of 104,408,289, and is_verified True. The same code against instagram returned 685,916,166 followers and 8,488 posts. The web_profile_info response is the richest single endpoint for a Python Instagram profile scraper: it carries the username, user id, full name, biography, follower and following counts, post count, profile picture URL, verified flag, and on professional accounts the public business_email and business_phone_number fields that an Instagram email scraper in Python reads. Those contact fields existed in the schema for both accounts I checked.
This is the core of scraping Instagram with Python, and it has two ceilings. The endpoint returns the profile and its most recent posts, but the full post history and the comments on a post come from a separate GraphQL query endpoint that needs a doc_id parameter, and Instagram rotates those identifiers every few weeks. The fingerprint trick also does not raise the rate limit: after a run of calls from one IP, the 429 returns regardless of curl_cffi. Residential IPs and a slow cadence push that ceiling back, which is the blocking problem covered two sections down. First, the library most people reach for when they want posts and comments without writing the GraphQL calls themselves.
How do you scrape Instagram with Instaloader?
You scrape Instagram with Instaloader by installing it from PyPI and either running the instaloader command or importing it as a Python library, which pulls public profiles, posts, comments, captions, and stories without a login for moderate volume. Instaloader downloads “pictures and videos along with their captions and other metadata,” per the project site, and it handles the GraphQL paging and doc_id problem for you, which is its main advantage over a hand-rolled curl_cffi script.
Install is one line:
pip3 install instaloader
The Python library gives you structured objects to parse. Here is the pattern for profile metadata plus the most recent post captions:
import instaloader
L = instaloader.Instaloader(download_pictures=False, download_videos=False)
profile = instaloader.Profile.from_username(L.context, "nasa")
print(profile.username, profile.followers, profile.mediacount)
print(profile.biography)
# Most recent 5 posts: shortcode, likes, caption
for i, post in enumerate(profile.get_posts()):
if i >= 5:
break
print(post.shortcode, post.likes, "-", (post.caption or "")[:60])
The workhorse methods are Profile.from_username(), get_posts(), get_followers(), and post.get_comments(). To scrape Instagram comments with Python, you iterate post.get_comments() on a Post object, and get_followers() walks a follower list, though both follower and comment access is where Instagram throttles anonymous use first. Instaloader is MIT-licensed and free, which is why it sits at the top of most instagram-scraper and python instagram scraper searches, and the maintained build was on version 4.15.1 as of March 2026 with roughly 12.6k GitHub stars.
Login is optional in Instaloader and the anonymous path works until Instagram answers with 401. That ceiling is shared by every public tool, including the curl_cffi script above and the older instagram-scraper CLI, which I cover alongside instagrapi in my roundup of open-source Instagram scrapers on GitHub. The next section measures the block directly, because understanding it decides whether you self-host or hand it off.
How do you scrape Instagram with Python without getting blocked?
You avoid the Instagram block by changing the two things that trigger it: the TLS fingerprint and the IP reputation, then slowing the request rate. These are the levers that moved the result in my testing, in rough order of impact.
- Match a browser fingerprint. Plain
requestsis refused at the handshake.curl_cffiwithimpersonate="chrome", or a headless browser, is what got a200for me. - Use residential or mobile IPs. Datacenter and cloud ranges are flagged on sight. A residential IP presents as an ordinary home connection, which raises the anonymous ceiling.
- Slow the cadence. Instagram tolerates a steady, human-like rate and punishes bursts. A few seconds between requests per IP is a safer starting point than parallel floods. The Scrapfly team puts the anonymous limit around 200 requests per hour per IP before a
429. - Expect rotating
doc_idvalues. Instagram changes the GraphQLdoc_ididentifiers every few weeks, which silently breaks scrapers that hardcode them. Use a library like Instaloader that tracks the current values, or an API that does. - Read public data only. A logged-in client moving fast draws a
challenge_requiredcheckpoint on the account, and private accounts return nothing without an approved session.
The honest tradeoff is maintenance. Doing all of this yourself means buying and rotating a residential proxy pool, keeping a fingerprint library current, retrying 401 and 429 responses, and re-deriving doc_id values every time Instagram changes them. That becomes a real engineering project once you pass a few thousand records or need many accounts on a schedule, which is the reason most teams hand the fingerprint, proxy, and parsing problem to a scraper API.
How do you scrape Instagram at scale with a scraper API?
You scrape Instagram at scale by sending one HTTP request to a scraper API that handles the TLS fingerprint, residential proxy rotation, and 401/429 retries on its servers, then returns parsed JSON. This removes the part of the Python routes above that breaks. You stop babysitting fingerprints, proxies, and doc_id values, and you get a profile, follower list, or comment thread back as structured data.
The request is a single GET with your target and an API key:
curl "https://chocodata.com/api/v1/instagram/profile?username=nasa&api_key=$CHOCO_API_KEY"
The Python version is the same shape, so it drops into an existing curl_cffi or Instaloader script as the fallback for when you start seeing 429s:
import requests, os
resp = requests.get(
"https://chocodata.com/api/v1/instagram/profile",
params={"username": "nasa", "api_key": os.environ["CHOCO_API_KEY"]},
timeout=30,
)
data = resp.json()
print(data["username"], data["followers"], data["biography"])
In my runs this returned the same profile fields the web_profile_info endpoint and Instaloader parse (username, followers, biography, mediacount) without a fingerprint library, a proxy, or a login on my side. The endpoint is authenticated, so a request without a valid key returns a not-found response, which is expected: when I called it with no key in June 2026 it returned 404 NOT_FOUND, confirming the host and path are live and gated. You can get an API key on the ChocoData sign-up page and point the same pattern at the profile endpoint, the follower endpoint, the comment endpoint, or the email and lead endpoint.
A follower scrape is the clearest case for the managed route, because the official API gives you nothing here. The Instagram Graph API IG User node exposes a followers_count number and no list of the accounts behind it, so every follower scraper reads the public roster instead. The API call returns that roster as JSON:
import requests, os
resp = requests.get(
"https://chocodata.com/api/v1/instagram/followers",
params={"username": "nasa", "api_key": os.environ["CHOCO_API_KEY"]},
timeout=30,
)
for follower in resp.json()["followers"]:
print(follower["username"], follower["full_name"], follower["is_verified"])
The tradeoff is the usual one. For a one-off pull of a few public profiles, curl_cffi or Instaloader from your own machine is free and fine. Once you need thousands of records, continuous collection, follower lists, or contact fields across many accounts, the maintenance of fingerprints and proxies costs more time than the API costs money. I rank the managed options head to head in my best Instagram scrapers in 2026 roundup.
Can you scrape Instagram in JavaScript or other languages?
You can scrape Instagram in JavaScript, Go, Ruby, PHP, or any language that sends an HTTP request and parses JSON, because Instagram’s public endpoints are plain HTTP and the data comes back as JSON. The method changes only in syntax, and the blocking problem is identical: every runtime’s default TLS fingerprint gets the request refused, exactly as Python’s requests did, so you need a fingerprint-matching client or a headless browser.
| Language | DIY scraping route | Fingerprint note |
|---|---|---|
| Python | curl_cffi, Instaloader, Playwright | requests is refused; curl_cffi impersonates Chrome |
| JavaScript / Node.js | Playwright, Puppeteer, a fetch with a TLS shim | Node’s default fetch is flagged like Python’s requests |
| Go / Ruby / PHP | A TLS-impersonating HTTP client or headless browser | Default HTTP clients carry detectable fingerprints |
| Any language | A scraper API over HTTP | The API matches the fingerprint server side |
A Playwright Instagram scraper is the common cross-language answer for the DIY path, because a real headless browser carries a real browser fingerprint and runs the page’s JavaScript, at the cost of being heavier and slower than a direct request. The scraper API route collapses the per-language problem to one HTTP call: the same GET request to the ChocoData endpoint returns the same JSON whether it comes from Python’s requests, Node’s fetch, or curl on the command line, because the fingerprint, proxies, and parsing live on the server. So picking a language for Instagram scraping comes down to which HTTP client you prefer, since every one of them faces the same fingerprint check at Instagram’s edge.
Before you collect at any scale, it is worth knowing where the line sits, because Instagram data is governed by both platform terms and privacy law. Meta’s Platform Terms state that you may not “access or collect data from our Products using automated means (without our prior permission),” and a profile’s follower list is personal data under regimes like the GDPR. A US federal court in Meta Platforms v. Bright Data (January 2024) held that Meta’s terms do not bar scraping of public, logged-off data, since a logged-out scraper never agrees to those terms, a ruling specific to public data that does not touch private content. I walk through what that does and does not cover in my guide on whether scraping Instagram is legal and what Instagram’s own rules say in Instagram’s terms of service on scraping; none of this is legal advice, so scrape only public data, respect the platform’s limits, and handle any personal data you keep under the law that applies to you.
FAQ
How do you scrape data from Instagram using Python?
You scrape data from Instagram using Python by requesting its public web endpoints with a library that matches a real browser's TLS fingerprint, because a plain requests call is refused. In my June 2026 tests, curl_cffi with impersonate="chrome" returned a profile's full JSON (username, follower count, biography, post count) from the web_profile_info endpoint, while the same request through requests returned a 429 with an empty body. Instaloader is the higher-level library for profiles, posts, and comments, and a scraper API returns parsed JSON without you managing the fingerprint or proxies.
Can you scrape Instagram with Python without logging in?
Yes, for public profiles and their posts and comments, but Instagram caps anonymous access hard. A Python Instagram scraper with no login can read a public profile through the web_profile_info endpoint or through Instaloader, then Instagram starts returning 401 'Please wait a few minutes' or 429 'Too Many Requests' after a handful of calls from one IP. Private accounts return nothing without an approved logged-in session, and the Scrapfly engineering team puts the anonymous ceiling near 200 requests per hour per IP.
Why does my Python Instagram scraper return a 429 error?
Your Python Instagram scraper returns a 429 because Instagram refuses the connection by IP reputation and TLS fingerprint before it serves any data. In my tests the requests library returned a 429 with a 0-byte body whether I sent a Chrome User-Agent, a Googlebot User-Agent, the x-ig-app-id header, or nothing at all, so the header is not the deciding factor. A datacenter or cloud IP and Python's default TLS signature are what trigger it. Matching a browser fingerprint with curl_cffi and using residential IPs is what cleared it.
Can you scrape Instagram followers and comments with Python?
You can scrape an Instagram follower list and post comments with Python, but neither comes from the official API. The Instagram Graph API returns a followers_count number and no follower list, so a Python follower scraper reads the public follower roster on the profile instead. Comments come from the GraphQL query endpoint, which needs a doc_id that Instagram rotates every few weeks, which is what breaks unmaintained scrapers. A managed follower scraper API or comment scraper API returns both as JSON without the cursor and doc_id work.
Can I scrape Instagram in JavaScript or another language?
Yes. Instagram's public endpoints are plain HTTP, so any language that sends an HTTP request and parses JSON can scrape them, including JavaScript and Node.js, Go, Ruby, and PHP. The catch is the same in every language: the runtime's default TLS fingerprint gets the request refused, so you need a fingerprint-matching client or a headless browser like Playwright. Sending the target to a scraper API sidesteps the per-language fingerprint problem, because the API handles it server side and returns the same JSON to any caller.