~ / guides / How to Scrape Instagram With Python (and Other Languages)

How to Scrape Instagram With Python (and Other Languages)

LH
Lena Hoff
Instagram data engineer · about the author
the short version
  • A plain requests.get to Instagram's web_profile_info endpoint 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 requests for curl_cffi with impersonate="chrome" returned 200 and 460 KB of profile JSON on the first try: username, follower count, bio, post count, and the public business_email field.
  • 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.

MethodWhat it pullsLogin neededHolds up at scaleMy note
requests (the naive call)Nothing, returns 429NoNoFails at the TLS layer before any data
curl_cffi (fingerprint match)Public profile JSONNoFor a while, then 429The fix that actually returned data for me
Instaloader (library)Profiles, posts, comments, storiesOptionalFor a while, then 401/429Best no-login library, MIT-licensed
Scraper APIProfiles, followers, comments, moreNoYesOne 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 requestsUser-Agent sentStatusBody
web_profile_info?username=nasafull Chrome desktop string4290 bytes
web_profile_info?username=nasanone4290 bytes
web_profile_info?username=instagraminstagramscraperapi.com/1.04290 bytes
web_profile_info?username=instagramGooglebot string4290 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.

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.

LanguageDIY scraping routeFingerprint note
Pythoncurl_cffi, Instaloader, Playwrightrequests is refused; curl_cffi impersonates Chrome
JavaScript / Node.jsPlaywright, Puppeteer, a fetch with a TLS shimNode’s default fetch is flagged like Python’s requests
Go / Ruby / PHPA TLS-impersonating HTTP client or headless browserDefault HTTP clients carry detectable fingerprints
Any languageA scraper API over HTTPThe 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.

LH
Lena Hoff
I've built Instagram data pipelines for years. On instagramscraperapi.com I run Instagram scraping methods against live pages and publish what actually holds up.