DEV Community

anassBld
anassBld

Posted on

Scraping the Meta Ad Library in 2026: cursors, one-shot browsers, and $0.11 per 1,000 ads

The Meta Ad Library is the best free competitor-research source there is: every active Facebook and Instagram ad, with copy, creatives, start dates and, for EU ads, reach by age, gender and country. The official API only covers political and EU ads, so for commercial ads you have to read what the website reads.

I built a scraper for it and recently packaged it to run hosted. Here's how the site actually loads ads, the three things that broke on the way, and what it costs per 1,000 ads.

How the page loads ads

The Ad Library is a React app. Scroll the results and it sends a GraphQL POST named AdLibrarySearchPaginationQuery. Each response holds a batch of ads plus a page_info object:

"page_info": { "has_next_page": true, "end_cursor": "AQHR..." }
Enter fullscreen mode Exit fullscreen mode

Send the same request again with cursor set to end_cursor and you get the next batch. That cursor chain is the whole scraper. Everything else is getting a request Meta will accept.

Lesson 1: use the browser once, not for every page

My first version scrolled a real Chrome window for every page. It worked, but it was slow, and every worker needed its own full browser.

What works better:

  1. Open one real browser and load a search page. It passes Meta's JavaScript checks and fires the first pagination request.
  2. Capture that request (form body and headers), plus the cookies and user agent.
  3. Close the browser and send the rest of the cursor chain from a plain HTTP client.

You change the search by rewriting the GraphQL variables (queryString, viewAllPageID, date filters) in the captured body. One page load can serve thousands of ads. When the session dies, open a new browser and resume from the last saved cursor, not from page one.

Result: 1,000 unique ads in about 3.5 minutes, and memory dominated by that single page load.

Lesson 2: "search by advertiser" needs its own search type

Searching for a page's ads (view_all_page_id) silently returned zero ads. The captured request still said searchType: KEYWORD_UNORDERED with an empty keyword, and Meta returns nothing for that. Page-only searches need searchType: PAGE. Zero results looked exactly like "this page has no ads", so check your zeros against the website before you trust them.

Lesson 3: containers break Chrome in boring ways

In Docker, the page crashed on load with Page crashed, even at 512 MB. It wasn't memory: Docker gives Chrome a 64 MB /dev/shm. Adding --disable-dev-shm-usage fixed it. Two more container details:

  • Running as root means Chrome won't start sandboxed. Disable the sandbox only when geteuid() == 0.
  • Meta blocks datacenter IPs on the first page load. Residential proxies work.

Dedup and dates

The same ad shows up under several searches and sometimes twice in one cursor chain. Key everything by ad_archive_id. start_date and end_date are Unix timestamps in seconds, not ISO strings.

Cost per 1,000 ads

Measured on hosted runs with a residential proxy:

Run Ads Time Cost
keyword search 50 40 s $0.024
keyword search 300 76 s $0.052
health check, 512 MB 3 46 s $0.0008

That's roughly $0.11 per extra 1,000 ads, nearly all of it proxy traffic. The JSON is about 13 KB per ad.

If you'd rather not run it yourself

I packaged it as an Apify actor: Facebook Ads Library Scraper. You enter keywords or page IDs and get deduplicated ads with EU reach data, at $0.60 per 1,000 ads. Calling it from Python:

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("githubanassk/meta-ad-library-scraper").call(run_input={
    "searchTerms": ["protein powder"],
    "country": "US",
    "activeStatus": "active",
    "maxAdsPerSearch": 500,
})
for ad in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(ad["page_name"], ad["start_date"], ad["text_body"][:80])
Enter fullscreen mode Exit fullscreen mode

What I'd like to hear about: which fields you actually use from the Ad Library, and whether anyone has found a reliable signal for ad performance beyond "how long has this ad been running".

Top comments (2)

Collapse
 
alkaznodemaven profile image
Alexandr Kazmin •

Do you keep the same proxy IP when switching from the browser to HTTP requests?

I've seen sessions get weird when the cookies stay the same but the exit IP changes. Curious if Meta cares about that or if you've been able to switch IPs without issues.

Collapse
 
anasbuilds997 profile image
anassBld •

Yes, I keep the same sticky proxy session between the browser bootstrap and the HTTP cursor chain.

If you rotate the exit IP mid-stream while reusing the browser's cookies, Meta's edge will occasionally choke or return empty pagination batches. Sticking to a 10–15 min sticky residential IP for the full cursor run keeps it reliable. Once the run wraps or the session expires, then I rotate and grab a fresh bootstrap.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.