DEV Community

Harsh Maur
Harsh Maur

Posted on

How I Find Winning Facebook Ads (Days Running Is Not Enough)

The usual advice for finding winning Facebook ads is simple: open the Meta Ad Library, find ads that have run for a long time, and copy what they do. An advertiser does not keep paying for an ad that loses money.

I tested that advice on 60 active US ads for "magnesium glycinate" on 20 October 2026. The median ad had already run for 129 days. 52 of the 60 were past the usual 30-day "winner" mark. When almost every ad is a "winner", the rule tells you nothing.

What did work: looking for the same hook launched more than once. When a brand keeps putting the same opening line into new ads, that hook is working for them. This post shows how to pull the ads, rank them, and find those repeated hooks with a short Python script.

I built the scraper used here and sell it on the Apify Store, so keep that in mind. All numbers below come from real runs.

Why "days running" breaks in a busy niche

The Ad Library shows ads that are active now. In a mature niche, most active ads are old, because the new ones that failed were already switched off. That is survivorship: the losers left the list before you looked. So:

  • In a new or small niche, "running 30+ days" is a useful filter.
  • In a busy niche like supplements, compare each ad against the niche median instead. Here the top quarter of ads had run 168 days or more.

What the Ad Library does not tell you

For normal commercial ads in the US, Meta shows no spend, no impressions and no reach. In my data, spend, impressions_lower and reach_lower were null for every ad. Meta shows spend and impressions only for political and issue ads. Ads shown in the EU and UK also show reach.

So run time and repetition are the best public signals of what is working. Neither is proof, but together they are much stronger than either alone.

Step 1: Pull the ads

Build your search on facebook.com/ads/library: choose a country, "All ads", and type a keyword. Copy the URL from the address bar. (Or build it with a free Ad Library URL builder, which also sets filters like media type and date.) Then run Facebook Ads Library Scraper with it:

{
  "urls": [{ "url": "https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=US&q=magnesium%20glycinate&search_type=keyword_unordered&media_type=all" }],
  "count": 60
}
Enter fullscreen mode Exit fullscreen mode

Facebook matches keywords loosely. In my run, 50 of the 60 ads mentioned magnesium somewhere in the ad (text, headline or cards). The other 10 were for other products, from general vitamin brands to pharmacy deals at Walgreens and CVS. If you need exact matches, filter on the ad text after the run. For a clean list of one competitor's ads, use their Page instead.

You can paste a brand's Facebook Page URL to get all ads from that one advertiser. You need no Facebook login and no Meta API token.

The run took 34 seconds. Each ad comes back as one flat row. These are the fields I used, from a real ad:

{
  "page_name": "Allison Arnold - Migraine Coach",
  "start_date_iso": "2026-06-07T07:00:00.000Z",
  "is_active": true,
  "ad_duration_days": 135,
  "is_long_running": true,
  "headline": "I Charge $189 for a Migraine IV Drip. My Patients Keep Asking About a $29 Roll-On I Keep at the Front Desk.",
  "cta_text": "Learn more",
  "display_format": "IMAGE",
  "link_url": "https://florusrelief.com/blogs/news/every-migraine-tool-you-have-works-downstream-of-the-source-here-is-what-works-at-it",
  "publisher_platform": ["FACEBOOK", "INSTAGRAM", "AUDIENCE_NETWORK", "MESSENGER", "THREADS"],
  "ad_library_url": "https://www.facebook.com/ads/library/?id=987150940600917",
  "spend": null
}
Enter fullscreen mode Exit fullscreen mode

ad_duration_days is the number of days between the start date and today (or the end date, for ads that stopped). is_long_running is true from 30 days on. The ad text, headline and media links sit at the top level, so the rows drop straight into a spreadsheet.

Step 2: Rank by days live and find repeated hooks

import os, re, statistics, requests
from collections import defaultdict

ACTOR = "harshmaur~facebook-ads-library-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
SEARCH = ("https://www.facebook.com/ads/library/?active_status=active&ad_type=all"
          "&country=US&q=magnesium%20glycinate&search_type=keyword_unordered&media_type=all")

ads = requests.post(
    f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={"urls": [{"url": SEARCH}], "count": 60},
    timeout=300,
).json()

# Catalog ads show a template placeholder (product.name in double braces), so their copy says nothing.
PLACEHOLDER = "{" * 2 + "product"
ads = [a for a in ads if PLACEHOLDER not in (a["headline"] or "") + (a["ad_text"] or "")]

days = [a["ad_duration_days"] for a in ads]
print(f"{len(ads)} ads, median {statistics.median(days):.0f} days live, "
      f"{sum(a['is_long_running'] for a in ads)} flagged long-running")

# Group the same opening line from the same page: one hook, many launches.
hooks = defaultdict(list)
for a in ads:
    opening = re.sub(r"\s+", " ", a["ad_text"] or a["headline"] or "")[:60]
    hooks[(a["page_name"], opening)].append(a)

def score(group):
    return (len(group), max(a["ad_duration_days"] for a in group))

for (page, opening), group in sorted(hooks.items(), key=lambda kv: score(kv[1]), reverse=True)[:6]:
    runs = sorted((a["ad_duration_days"] for a in group), reverse=True)
    print(f"\n{page}: {len(group)} ad(s), days live {runs}")
    print(f"  \"{opening}...\"")
    print(f"  {group[0]['display_format']} -> {group[0]['link_url']}")
Enter fullscreen mode Exit fullscreen mode

Two details matter:

  • Drop catalog ads first. 8 of my 60 ads were catalog (dynamic product) ads. Their headline is literally the placeholder product.name in double curly braces, because Meta fills it in per viewer. They run for months, but their copy teaches you nothing.
  • Group by page and opening line. The first 60 characters of the ad text are the hook. The same page reusing the same hook in a new ad is the signal.

The real output

52 ads, median 121 days live, 45 flagged long-running

Restelle: 4 ad(s), days live [210, 69, 52, 39]
  "Tired of restless nights? 😴 Introducing our NEW Sleep Patche..."
  IMAGE -> https://myrestelle.com/pages/lp-sleep-2

Allison Arnold - Migraine Coach: 2 ad(s), days live [213, 151]
  "I tracked my migraines for 2,190 days. It took me six years ..."
  IMAGE -> https://florusrelief.com/blogs/news/every-migraine-tool-you-have-works-downstream-of-the-source-here-is-what-works-at-it

We Heart Nutrition: 2 ad(s), days live [211, 187]
  "Why our magnesium glycinate isn't like the others... Researc..."
  IMAGE -> https://www.weheartnutrition.com/

Top 5 Choices: 2 ad(s), days live [155, 117]
  "We've tested them all you don't have to...."
  VIDEO -> https://top5-choices.com/magnesiumreview

Best Supplement Finds: 2 ad(s), days live [152, 133]
  "We tested 47 magnesium supplements for brain fog, cortisol, ..."
  VIDEO -> https://goldsealsupplements.com/pages/magnesium
Enter fullscreen mode Exit fullscreen mode

(The script printed one more group. I cut it here to keep the list short.)

What the winners have in common

1. Restelle ran one hook in four ads. The first ad has run 210 days. Then they added the same opening line in new ads at 69, 52 and 39 days. A brand does that when one message keeps converting and they want more creative variety around it. That is the strongest "winner" signal in the whole dataset.

2. The landing page is often not the store. Look at the links. "Allison Arnold - Migraine Coach" sends people to a blog post on florusrelief.com. "Top 5 Choices" and "Best Supplement Finds" send people to "we tested them all" review pages. These are advertorials: a story or a comparison page that sells before the product page. Several of the longest-running ads in this niche use this pattern.

3. Several hooks are first-person stories with exact numbers. "I tracked my migraines for 2,190 days." "We tested 47 magnesium supplements." "I charge $189 for a migraine IV drip." None of the long-running hooks open with the product name.

4. Video and image both win. Of the 60 ads, 27 were video and 18 were image. The repeated hooks above are split between the two, so format alone was not the deciding factor here.

Turn it into a weekly swipe file

  1. Save the input as a task in Apify Console and schedule it weekly.
  2. Keep ad_archive_id from each run. An ad that shows up week after week, and a hook that gets new ad IDs, are the ones to study.
  3. To follow one competitor, paste their Page URL instead of a keyword search.
  4. For EU and UK ads, turn on scrapeAdDetails to also get reach, age and gender breakdowns and target countries.

What it costs

$0.50 per 1,000 ads, with no start fee. My 60-ad run cost 3 cents. A weekly run of 500 ads is about $1 a month.

FAQ

Can I see how much a competitor spends on Facebook ads?
Not for normal commercial ads in the US. Meta publishes spend only for political and issue ads. Ads shown in the EU and UK come with reach numbers, which are the closest public proxy.

What counts as a long-running Facebook ad?
There is no official definition. 30 days is a common rule of thumb, and it is what is_long_running uses. In a busy niche, compare against the niche median instead, as shown above.

Does a long-running ad always mean it is profitable?
No. Some brands leave ads on by mistake, and catalog ads run for months by design. Repetition (the same hook in several new ads) plus run time is a much stronger signal than run time alone.

Do I need a Facebook account or Meta API access?
No. The scraper reads the public Ad Library, the same one anyone can open without logging in.

Try it

Facebook Ads Library Scraper on Apify. Paste your niche's Ad Library URL, set count to 60, and sort the results by ad_duration_days.

Want an exact-phrase search, a date window or one platform only? Build the URL with this free Facebook Ad Library search URL builder, open it to check the ads, then paste it into urls.

What signal do you use besides run time? Tell me in the comments. I would like to add it to the script.

The links to my Actor include my Apify referral code.

Top comments (0)