How to scrape Booking.com at scale in 2026 with proxies that work
Booking.com is one of the harder travel sites to scrape reliably. Prices shift by IP location and currency, listings render through JavaScript, and the anti-bot layer gets more aggressive every year as they fight off rate-comparison tools, resellers, and yield-management scrapers competing with their own affiliate program. If you’ve tried pulling hotel prices with a basic requests script and gotten empty pages or a wall of CAPTCHAs, you’re not doing anything wrong, that’s just what happens when you hit it without proxies or session handling.
This tutorial is for developers and small ops teams building a price-monitoring or market-research pipeline: tracking rate parity across OTAs, feeding a revenue management tool, or building a travel meta-search dataset. I’m going to walk through a working setup using Playwright, rotating residential proxies, and rate limiting that keeps your block rate low enough to run daily. By the end you’ll have a scraper that pulls structured hotel listing data from search results pages and a sense of what changes as you scale from a few hundred requests a day to tens of thousands.
Before anything else: read Booking.com’s terms of service and decide your own risk tolerance. This article covers the technical side, not legal advice, and you should treat it as such.
what you need
- Python 3.11 or newer, or Node.js if you prefer Puppeteer over Playwright
- Playwright installed with the Chromium browser binary
- A rotating residential or ISP proxy plan with city-level geo-targeting (I’ve reviewed a few on this site, see decodo-review-2026-honest-pros-cons-and-pricing for one option)
- BeautifulSoup or an equivalent HTML parser for structured extraction
- Somewhere to store output: Postgres or SQLite is fine at small scale, don’t start with flat CSVs if you plan to scale past a few thousand rows a day
- A budget line for proxy bandwidth. Residential proxy plans in mid-2026 run roughly $4 to $8 per GB depending on provider and volume tier, and a single Booking.com search results page with images blocked runs somewhere around 200 to 400 KB
- Patience for iterating on CSS selectors, Booking.com changes its frontend markup often enough that a script written in January can break by March
step by step
1. Check Booking.com’s robots.txt and terms before writing code
Pull up booking.com/robots.txt in a browser and read it directly, don’t assume it matches what some other article claims. Cross-reference against the terms of service, which restricts automated data collection. In the US, the leading case on scraping publicly accessible web data against a site’s terms is hiQ Labs v. LinkedIn, which found that scraping public data generally doesn’t violate the Computer Fraud and Abuse Act, but that ruling doesn’t erase contract-law exposure from violating a site’s terms, and outcomes vary by jurisdiction and by what data you’re collecting. This is not legal advice, if you’re building anything commercial on top of this data, get a lawyer to look at your specific use case.
Expected output: a clear picture of what’s disallowed and where your actual risk sits. If it breaks: if you decide the risk is too high for your use case, Booking.com does run an official affiliate program with a data feed, that’s the compliant alternative for a lot of use cases people reach for scraping first.
2. Set up your environment
pip install playwright beautifulsoup4
playwright install chromium
Expected output: the Chromium binary downloads and Playwright can launch a headless browser without errors.
If it breaks: on a locked-down corporate network the binary download often fails silently. Run playwright install --with-deps chromium on Linux, or set PLAYWRIGHT_BROWSERS_PATH to a writable directory and retry.
3. Get a proxy plan built for geo-sensitive price data
Booking.com serves different prices and currencies depending on the requesting IP’s country and sometimes city. A datacenter proxy in Frankfurt will show you different rates than a residential IP in Singapore for the identical search. You need a provider with real city-level geo-targeting, not just country-level. I’ve written up a few proxy providers with travel and local-search use cases in mind, worth a look before you commit to a plan: best-proxies-for-scraping-local-search-results-in-2026.
Expected output: a gateway endpoint and credentials, something like gate.provider.com:7000 with a username that encodes country and city targeting.
If it breaks: if prices look identical across proxies you thought were in different cities, verify the exit IP’s actual geolocation with a lookup tool before blaming your scraper logic, some “residential” pools are thinner than advertised outside major cities.
4. Write the base scraper for one search page
from playwright.sync_api import sync_playwright
PROXY = {
"server": "http://gate.yourprovider.com:7000",
"username": "user-country-sg-city-singapore",
"password": "yourpassword",
}
def fetch_search_page(url):
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY, headless=True)
page = browser.new_page(locale="en-US")
page.goto(url, timeout=30000)
page.wait_for_selector('[data-testid="property-card"]', timeout=15000)
html = page.content()
browser.close()
return html
Expected output: raw HTML containing rendered hotel listing cards, not the pre-JS skeleton page.
If it breaks: if wait_for_selector times out, the page likely rendered a CAPTCHA or redirect instead. Screenshot the page (page.screenshot(path="debug.png")) before closing the browser to see what you actually got.
5. Parse listings into structured records
from bs4 import BeautifulSoup
def parse_listings(html):
soup = BeautifulSoup(html, "html.parser")
listings = []
for card in soup.select('[data-testid="property-card"]'):
name = card.select_one('[data-testid="title"]')
price = card.select_one('[data-testid="price-and-discounted-price"]')
listings.append({
"name": name.get_text(strip=True) if name else None,
"price": price.get_text(strip=True) if price else None,
})
return listings
Treat those data-testid selectors as illustrative, not gospel. Open devtools on the actual current page and confirm the attributes before you trust this in production.
Expected output: a list of dicts with hotel name and price per listing. If it breaks: empty results usually mean Booking.com renamed a component or shifted markup. This happens often enough that I’d budget a selector-maintenance pass roughly monthly.
6. Add rotation, delays, and sticky sessions
Rotate your exit IP per search session rather than per request within a session, Booking.com’s currency and price display depend on session cookies staying consistent, and swapping IPs mid-session can produce inconsistent or stale prices.
import time, random
def polite_delay():
time.sleep(random.uniform(4, 9))
Expected output: a request pattern that doesn’t look like a burst, spread over several seconds per page with new proxy sessions per search. If it breaks: rising 403s or CAPTCHA rates mean your timing or fingerprint still looks automated. Rotating the IP alone often isn’t enough, TLS and browser fingerprint consistency matters too. If you’re also dealing with fingerprinting on other sites, antidetectreview.org covers browser fingerprint tooling in more depth than I will here.
7. Handle CAPTCHAs and bot checks gracefully
When a session gets flagged, don’t retry immediately on the same proxy, that just confirms to the anti-bot system that the IP is automated. Mark that session as burned, drop it, and pick up the search with a fresh proxy session later. Don’t route around CAPTCHAs with third-party solving services against Booking.com’s explicit terms, that’s a fast way to get an entire proxy subnet blacklisted, not just one session.
Expected output: a declining CAPTCHA rate as your pacing and fingerprint hygiene improve.
If it breaks: if CAPTCHA rate stays high across a fresh proxy pool, the issue usually isn’t the proxy, it’s headless browser detection. Confirm your Playwright context isn’t leaking obvious automation signals (missing plugins, navigator.webdriver set to true, etc.) before you spend more on proxies.
8. Store, timestamp, and monitor
Write every record with a scrape timestamp and search parameters (dates, city, currency), since Booking.com prices change by the hour. Dedupe on hotel ID plus check-in date plus length of stay, not on raw text, since price text formatting varies by currency.
Expected output: a growing table you can query for price trends without duplicate or ambiguous rows. If it breaks: if you see wildly different prices for the same hotel and date, check whether currency or a promotional banner (member pricing, mobile-only rate) slipped into your parsed price field.
common pitfalls
Treating this like a static HTML site. Booking.com’s search results render client-side and depend on session state. A plain requests.get() call without a real browser context will mostly return a shell page with no listings.
Ignoring geo-consistency within a session. Switching proxy IPs mid-search (say, between the search page and a property detail page) can produce mismatched currency or stale pricing that looks valid but isn’t.
Underestimating how often the DOM changes. Booking.com ships frontend changes regularly. If your pipeline has no alerting for “listings extracted dropped to zero,” you’ll find out days later when someone asks why the dataset is empty.
Burning through proxy budget on retries instead of backing off. A tight retry loop on a flagged proxy wastes bandwidth and accelerates that IP getting fully blacklisted. Back off and rotate instead of hammering.
Scraping more than you need. If you only need price and availability, don’t also pull guest review text and reviewer names. That data carries more legal weight under privacy law like the EU’s GDPR if you’re operating in or targeting the EU, and it’s usually not what your use case actually requires.
scaling this
At 10x (a few hundred to a couple thousand requests a day), a single script on one VM with a modest rotating proxy pool is enough. Run it on a cron schedule, log block rates, done.
At 100x (tens of thousands of requests a day), you need a queue (Redis or a simple job table works) feeding multiple worker processes, each with its own proxy session pool. You’ll want per-city or per-market request budgets so one hot market doesn’t starve the rest of your pool, and a dashboard tracking block rate by proxy subnet, not just overall.
At 1000x (hundreds of thousands of requests a day), bandwidth cost becomes the dominant line item, not engineering time. At this volume it’s worth spreading traffic across more than one proxy provider so a single provider’s subnet getting flagged doesn’t take down your whole pipeline, and it’s worth seriously reconsidering whether Booking.com’s affiliate data feed covers your actual need before continuing to scrape at that volume. If you’re running scraping alongside other multi-account or multi-identity operations, the session and identity management patterns overlap a lot with what multiaccountops.com covers for keeping separate operational identities from bleeding into each other.
where to go next
If you’re still choosing a proxy provider for this, start with decodo-review-2026-honest-pros-cons-and-pricing and compare it against decodo-vs-smartproxy-2026-head-to-head-comparison to see how pricing and geo-coverage stack up for travel data specifically. For the browser fingerprinting side of anti-bot evasion that this tutorial only touched on, antidetectreview.org/blog goes deeper. And for more scraping tutorials targeting other travel and local-search platforms, check the full archive.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-07-14.