SERP & SearchElite tier

Scraping Yandex and International Search Engines for Rank Tracking in Non-Google Markets

Yandex owns roughly 60% of Russian search and has no public organic-results API, so rank trackers have to query the public SERP pages directly - and Yandex runs one of the most aggressive anti-automation stacks outside Google, from its showcaptcha 'are you a robot' interstitial to IP-reputation and request-velocity scoring. On top of that, results are region-personalized through the lr= region code, so an exit in the wrong country returns rankings nobody in your target market actually sees. Here is how to collect accurate, region-correct public SERP data at reasonable volume without drowning in robot checks.

MTWritten by Marcus Thorne·Reviewed by Liam O'Connor·Updated September 4, 2026
13 min read
Editorial board contrasting a Yandex showcaptcha robot interstitial with organic results for lr=213 Moscow.

Key takeaways

  • Yandex blocks on IP reputation + request velocity + fingerprint together, not raw request count - throttling requests alone rarely clears a showcaptcha wall once your exit is flagged.
  • Yandex SERPs are region-personalized: the lr= parameter (e.g. lr=213 for Moscow) plus the exit country decide the ranking you see, so exits must sit in the target market with -country-ru.
  • The /showcaptcha redirect and the captcha JSON field are soft blocks - a 200 OK page can still be a challenge page, so parse for the challenge before you trust results.
  • Per-request rotation is the right default; a fresh residential exit per query keeps any single IP well under Yandex's velocity threshold.
  • This is public, non-personal SERP research - keep volume proportionate to genuine rank-tracking needs and respect each engine's Terms of Service.

On this page

What goes wrong when this scraper fails

Rank tracking outside the Google monoculture means scraping engines that guard their SERPs just as hard, with different tells. Yandex dominates Russia and much of the CIS, Baidu leads China, Naver leads South Korea, and Seznam holds real share in the Czech market - none expose a usable public organic-ranking API, so trackers read the HTML pages that human searchers see. Yandex in particular answers suspicious traffic with its showcaptcha interstitial (a SmartCaptcha 'are you a robot' page served at /showcaptcha), scores exits on IP reputation and request velocity, and personalizes every result set by region and language. Get the exit country or region code wrong and you are not blocked - worse, you silently store rankings that no real user in that market would ever see.

Why this failure mode happens

Yandex fingerprints incoming traffic across several axes at once: the reputation of the exit IP (datacenter ranges and known proxy pools are pre-scored poorly), the velocity and regularity of requests from that IP, and the completeness of the request - a bare User-Agent with no Accept-Language, no cookies, and no realistic header ordering reads as automation instantly. When the combined score crosses a threshold Yandex redirects to /showcaptcha or embeds a captcha token in the response instead of results. Because rankings are computed per region via lr= and per language via the interface params, two identical queries from a Moscow exit and a Frankfurt exit return genuinely different orderings - the personalization is a feature of the product, not an anti-bot trick, but it means location correctness is as important as not getting blocked.

Form pairing the lr region code with a country-ru residential exit for region-correct rankings.
lr= plus the exit country decide the ranking. An exit in the wrong country returns the wrong SERP.

Challenges that make this hard to automate

  • The showcaptcha interstitial: once your exit trips the score, Yandex serves a SmartCaptcha page at /showcaptcha with a 200 status, so a naive scraper stores an empty or challenge page as if it were results.
  • Region personalization: the same keyword ranks differently by lr= region code and by exit country, so a wrong-country exit produces plausible-but-wrong data that is hard to notice.
  • IP reputation scoring: datacenter and flagged proxy ranges start with a poor score and hit captchas within a handful of requests regardless of pacing.
  • Velocity detection: bursts of regular-interval requests from one exit are scored as automation even at modest per-minute rates.
  • Fragile, shifting markup: Yandex rotates class names and DOM structure, and Baidu/Naver each have their own layout quirks, so brittle selectors break parsing silently and return zero results.

Approaches that usually fail

  • Datacenter proxy lists: cheap and fast, but Yandex has pre-scored most datacenter ranges poorly, so they hit showcaptcha almost immediately and burn out.
  • Headless browser farms with no IP strategy: Playwright or Puppeteer can render the page, but without residential exits in the target market they still trip reputation scoring and just render the captcha instead.
  • Third-party SERP APIs: convenient, but coverage of Yandex and regional engines is thin, region-code control is often coarse, and you inherit their block-recovery and freshness rather than owning it.
  • Manual captcha-solving pipelines: routing every showcaptcha through a human/solver service is slow, costly, and treats the symptom - a good exit strategy means you rarely see the challenge at all.

When residential proxies fix this — and when they cannot

The single biggest lever is where the request exits and how often that exit is reused. Residential exits inside the target market (a Moscow or St. Petersburg IP for a Russian query) carry the reputation of a real subscriber line, so they start with a clean score instead of the pre-flagged datacenter penalty, and they match the region personalization you actually want to measure. Rotating to a fresh exit on every request keeps any single IP far below Yandex's velocity threshold, so no one address accumulates the request regularity that triggers scoring. Combined with a complete, realistic header set and the correct lr= region code, per-request residential rotation turns rank tracking from a captcha-fighting exercise into a data-collection one.

Session rail rotating a fresh RU residential exit on every Yandex query to stay under velocity.
Per-request rotation keeps any single IP under Yandex's velocity threshold.

How Aethyn residential proxies help here

Aethyn gives you residential exits in the exact markets these engines personalize for, addressed with one compact username convention. For Yandex work you point at a Russian exit pool and let the network rotate a fresh IP per query, which is precisely the pattern that keeps you under velocity scoring while matching the region you are trying to measure. The Elite tier is the right pick for SERP work because its exit reputation and pool depth hold up against Yandex's IP scoring far better than shared datacenter ranges.

  • Residential exits in Russia and the CIS (and other regional markets) so your SERP matches the lr= region you are tracking, addressed with -country-ru.
  • Per-request rotation by default: omit the session token and every request leaves on a fresh exit, staying under Yandex's velocity threshold automatically.
  • Elite-tier exit reputation (port 5499) tuned for high-scrutiny targets, so exits arrive with a clean score instead of a pre-flagged datacenter penalty.
  • Optional sticky sessions (-session-TOKEN-lifetime-MINUTES, up to 30 min) for the rare multi-request flow that must stay on one exit, such as paging deep into a single result set.
  • City-level targeting (-city-moscow) when a keyword's ranking is metro-specific and the region code alone is too coarse.
  • One username format across every engine you track - Yandex, Baidu, Naver, Seznam - so the same client code just changes the country code.

How to implement this with residential proxies

  1. 1

    Build the region-correct proxy exit

    Rankings are personalized, so the exit country and the lr= region code must agree with the market you are measuring. For Russian rank tracking, exit through a Russian residential IP and pass the matching Yandex region code (lr=213 is Moscow, lr=2 is St. Petersburg, lr=225 is all of Russia). Use per-request rotation - no session token - so each query leaves on a fresh exit. The Elite tier uses port 5499.

    python
    import requests
    
    # Elite tier -> port 5499. Per-request rotation: no session token.
    PROXY = "http://aethyn-XXXXX-country-ru:PASSWORD@proxy.aethyn.io:5499"
    proxies = {"http": PROXY, "https": PROXY}
    
    params = {
        "text": "купить кроссовки",   # query
        "lr": 213,                    # Yandex region code: 213 = Moscow
        "lang": "ru",
        "p": 0,                       # page index (0-based)
    }
    
    resp = requests.get(
        "https://yandex.ru/search/",
        params=params,
        proxies=proxies,
        timeout=20,
    )
    print(resp.status_code, resp.url)

    Field note: Keep a small lookup of Yandex lr= codes per city you track and store it alongside every stored ranking. A ranking row without its region code is unreconstructable later - you will not be able to tell whether position 4 was a Moscow result or a nationwide one.

  2. 2

    Send a complete, human-realistic header set

    A bare User-Agent with no Accept-Language is the classic bot tell, and for a region-personalized engine the language header also shapes results. Send a full, coherent header set: a current browser User-Agent, an Accept-Language that matches the market (ru-RU), a real Accept, and an Accept-Encoding. Keep the headers internally consistent - a Chrome User-Agent paired with a Firefox-style Accept ordering is itself a fingerprint.

    python
    HEADERS = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/126.0.0.0 Safari/537.36"
        ),
        "Accept": (
            "text/html,application/xhtml+xml,application/xml;q=0.9,"
            "image/avif,image/webp,*/*;q=0.8"
        ),
        "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Upgrade-Insecure-Requests": "1",
    }
    
    resp = requests.get(
        "https://yandex.ru/search/",
        params=params,
        headers=HEADERS,
        proxies=proxies,
        timeout=20,
    )

    Field note: Match Accept-Language to the exit country, not to your own locale. A Russian exit sending Accept-Language: en-US is a contradiction Yandex can score against - the header should look like the browser of a real user in that market.

  3. 3

    Detect the showcaptcha challenge before trusting the response

    Yandex returns the SmartCaptcha challenge with a 200 status, so status code alone means nothing. Treat a response as blocked if it redirects to /showcaptcha, contains the showcaptcha markup, or (on the XML/JSON surfaces) carries a captcha field. Never write a challenged response to your database - classify it, back off, and retry on a fresh exit.

    python
    def is_blocked(resp):
        # Yandex serves the SmartCaptcha page with HTTP 200.
        if "/showcaptcha" in resp.url:
            return True
        body = resp.text
        markers = (
            "showcaptcha",
            "SmartCaptcha",
            "captcha-required",
            "я не робот",  # 'I am not a robot'
        )
        return any(m in body for m in markers)
    
    if is_blocked(resp):
        raise RuntimeError("showcaptcha challenge - rotate exit and back off")

    Field note: Log the block rate per region and per hour, not just per run. A creeping showcaptcha rate is the earliest signal that your pacing or header set has drifted into detection - catch it at 5% before it becomes 60%.

  4. 4

    Parse defensively and assert a non-empty result set

    Yandex rotates class names and DOM structure, so anchor parsing on stable containers (organic result items carry a serp-item / li.serp-item structure) and extract title, URL, and position with fallbacks. Then assert you actually got results before storing - a 200 OK with zero parsed organic results on a common query is almost never a real empty page, it is a soft block or a markup change.

    python
    from bs4 import BeautifulSoup
    
    def parse_organic(html):
        soup = BeautifulSoup(html, "html.parser")
        results = []
        for pos, item in enumerate(soup.select("li.serp-item"), start=1):
            link = item.select_one("a.organic__url, a.Link")
            title = item.select_one("h2, .organic__title")
            if not link or not link.get("href"):
                continue
            results.append({
                "position": pos,
                "url": link["href"],
                "title": title.get_text(strip=True) if title else "",
            })
        return results
    
    rows = parse_organic(resp.text)
    if not rows:
        raise RuntimeError("zero organic results - treat as soft block, do not store")

    Field note: Always assert a non-empty result set before writing to your database. A 200 OK with zero parsed results is almost never a real empty page - it is a soft block or a silent markup change, and storing it is how a rank dashboard goes quietly wrong for weeks.

  5. 5

    Pace, rotate, and back off on blocks

    Per-request rotation handles velocity, but add jittered delays so your traffic does not arrive on a machine-regular cadence, and implement exponential backoff when you do see a challenge. On a block, rotate to a fresh exit (which happens automatically with no session token) and increase the delay before retrying rather than hammering the same query. Keep concurrency modest - a few workers per region, not hundreds.

    python
    import random, time
    
    def fetch_with_backoff(params, max_tries=4):
        delay = 2.0
        for attempt in range(max_tries):
            # Fresh exit every call (no session token in the username).
            resp = requests.get(
                "https://yandex.ru/search/",
                params=params, headers=HEADERS, proxies=proxies, timeout=20,
            )
            if not is_blocked(resp):
                return resp
            time.sleep(delay + random.uniform(0, 1.5))  # jittered backoff
            delay *= 2
        raise RuntimeError("exhausted retries - persistent challenge")
    
    time.sleep(random.uniform(1.0, 4.0))  # jitter between queries
    resp = fetch_with_backoff(params)

    Field note: Cap retries at 3-4 and record persistent failures rather than looping forever. A query that will not clear after four fresh exits is telling you something structural - a bad region code, a flagged pool, or a real markup change - and infinite retries just burn exits and inflate your block rate.

  6. 6

    Reuse the same pattern for other regional engines

    The exit-in-market plus complete-headers plus defensive-parse pattern ports directly to Baidu (China, -country-cn), Naver (South Korea, -country-kr), and Seznam (Czech Republic, -country-cz). Only three things change per engine: the exit country code in the username, the query/region parameters, and the result selectors. Keep one engine adapter interface so adding a market is a config change, not a rewrite.

    python
    ENGINES = {
        "yandex": {"country": "ru", "url": "https://yandex.ru/search/",
                    "text_param": "text", "lang": "ru-RU"},
        "baidu":  {"country": "cn", "url": "https://www.baidu.com/s",
                    "text_param": "wd", "lang": "zh-CN"},
        "naver":  {"country": "kr", "url": "https://search.naver.com/search.naver",
                    "text_param": "query", "lang": "ko-KR"},
    }
    
    def proxy_for(country):
        # Elite tier -> port 5499, per-request rotation.
        user = "aethyn-XXXXX-country-" + country
        return "http://" + user + ":PASSWORD@proxy.aethyn.io:5499"
    
    cfg = ENGINES["naver"]
    p = {"http": proxy_for(cfg["country"]), "https": proxy_for(cfg["country"])}
    r = requests.get(cfg["url"], params={cfg["text_param"]: "운동화"}, proxies=p, timeout=20)

    Field note: Give every engine adapter its own is_blocked and parse function - Baidu's challenge and Naver's empty-state look nothing like Yandex's showcaptcha. A shared block detector tuned only for Yandex will silently misclassify the others and quietly poison those markets' data.

Best practices that keep scrapers reliable

  • Collect only public, non-personal SERP data - the ranking of public pages for a keyword - keep request volume proportionate to real rank-tracking needs, and respect each engine's Terms of Service; consult counsel for your jurisdiction.
  • Always store the region context (exit country plus lr= or engine region code) with every ranking row, so results are reproducible and never ambiguous between metro and national rankings.
  • Default to per-request rotation and reserve sticky sessions for the rare flow that must stay on one exit, such as paging deep into one result set within the 30-minute lifetime cap.
  • Assert a non-empty, sane result set before persisting, and treat zero-result or challenge responses as soft blocks to retry, never as data to store.
  • Match the exit country, Accept-Language, and region code to each other and to the target market - any mismatch is both a fingerprint and a data-correctness bug.
  • Track block rate and result-count distributions per region and hour as first-class metrics, so pacing or markup drift surfaces early instead of after weeks of bad data.
  • Keep engine-specific block detectors and parsers isolated behind an adapter so one engine's markup change or challenge style never corrupts another market's results.

Common mistakes that burn proxy budget

  • Trusting the HTTP status: Yandex serves showcaptcha with a 200, so treating status 200 as success stores challenge pages as rankings.
  • Using datacenter exits for Yandex: those ranges are pre-scored poorly and hit showcaptcha within a few requests no matter how slowly you pace.
  • Ignoring the lr= region code (or using an exit outside the target country), producing plausible but wrong rankings that no user in that market sees.
  • Sending a bare User-Agent with no Accept-Language, or an Accept-Language that contradicts the exit country - both read as automation.
  • Storing zero-result responses as legitimate empty SERPs instead of flagging them as soft blocks or markup changes.
  • Reusing a Yandex-tuned block detector and selectors for Baidu, Naver, or Seznam, which silently misclassifies and mis-parses those engines.

Common questions about this scraper problem

Is scraping Yandex search results legal?
Collecting public, non-personal SERP data - the public ranking of public pages for a keyword - is a common SEO research practice, but it can still conflict with an engine's Terms of Service. Collect only public, non-personal data, keep volume reasonable and proportionate to genuine rank tracking, respect each engine's ToS and robots directives, and consult counsel for your specific jurisdiction and use case.
Why do I hit the 'are you a robot' page even at low request rates?
Yandex scores on IP reputation and fingerprint, not just velocity. A datacenter or flagged proxy range starts with a poor reputation score, so it can trip the showcaptcha interstitial within a handful of requests regardless of how slowly you pace. Residential exits in the target market start with a clean score, which is usually the fix.
What is the lr= parameter and do I need it?
lr= is Yandex's region code, and it directly changes the ranking - lr=213 is Moscow, lr=2 is St. Petersburg, lr=225 is all of Russia. Because SERPs are region-personalized, you should set lr= to the region you are tracking and pair it with an exit in that country, or you will measure a ranking that no one in your target market actually sees.
Should I use per-request rotation or sticky sessions?
Per-request rotation is the right default for rank tracking: a fresh exit per query keeps any single IP under Yandex's velocity threshold. Use a sticky session (up to a 30-minute lifetime) only when several requests must stay on one exit, such as paging deep into a single result set where switching IPs mid-flow looks unnatural.
How do I know if a 200 response is actually a captcha page?
Check for the challenge before parsing: a redirect to /showcaptcha, showcaptcha or SmartCaptcha markup in the body, or a captcha field on the XML/JSON surfaces. Treat any of those as a soft block, back off, and retry on a fresh exit rather than storing the page.
Do I need a headless browser like Playwright for Yandex?
Often not - a plain requests call with a complete header set and a good residential exit retrieves the organic SERP HTML directly, which is cheaper and faster. Reach for Playwright only when you need JavaScript-rendered elements or want a fuller browser fingerprint for the hardest exits; the IP strategy matters more than the rendering engine.
Why are my parsed results suddenly zero when the page loads fine in a browser?
Two common causes: Yandex rotated its class names so your selectors no longer match, or you received a soft block that returned a challenge or thin page. Anchor your parser on stable containers with fallbacks, and always assert a non-empty result set so a zero-result response is flagged for investigation instead of stored.
Which Aethyn tier and port should I use for this?
Use the Elite tier on port 5499. SERP targets like Yandex score exits hard on reputation, and Elite exits arrive with a cleaner score and deeper pool than shared datacenter ranges, which materially lowers your showcaptcha rate. The proxy URL looks like http://aethyn-XXXXX-country-ru:PASSWORD@proxy.aethyn.io:5499.
Can the same setup track Baidu, Naver, or Seznam?
Yes - the pattern is identical, only the exit country code, query parameters, and result selectors change. Point at -country-cn for Baidu, -country-kr for Naver, and -country-cz for Seznam, and give each engine its own block detector and parser since their challenge and empty-state markup differ from Yandex's.
How much volume is 'reasonable' for rank tracking?
Enough to track the keywords and regions you genuinely report on, refreshed at the cadence your dashboards need - typically daily or weekly - not continuous high-rate crawling. Keeping volume proportionate to real reporting needs is both the responsible posture toward the engine's ToS and, practically, the pattern least likely to draw sustained anti-bot attention.
Share this article

Solve this with Aethyn residential proxies

Route your first request through rotating residential IPs in minutes, with transparent per-byte pricing.

All solutions