How to Scrape Google Flights With Python in 2026
Google Flights is not your average scraping target. The page is almost entirely JavaScript-rendered, prices change based on the country your IP is from (not just URL parameters), and silent failures are more common than actual blocks. Here are the three approaches that actually work in 2026.
- For quick, one-off lookups: the
fast-flightsPython library (pip install fast-flights) skips the browser entirely and hits Google's protobuf-encoded endpoint directly. It's fast, typed, and returns structured data in seconds. Doesn't scale to high-frequency use without a proxy though. - For more robust data extraction: Playwright with rotating residential proxies. Playwright renders the full JavaScript-heavy page. Residential proxies provide geo-accurate pricing per target market. Note that automated access violates Google's Terms of Service, so assess legal risk for your specific use case before proceeding.
- Country matters more than you think. A JFK → LHR search from a US proxy IP will return different prices than the same search from a UK proxy IP. URL parameters alone don't fix this. Your proxy country needs to match your target market.
- Silent failures are the real problem. Google Flights doesn't always block you outright. Sometimes it just returns a page with no flight data in it. Always validate that your scraper actually got results, not just a 200 status code.
- When to skip DIY scraping entirely: If you're building a commercial booking tool or need guaranteed real-time accuracy, use the Amadeus API or a licensed SERP API. The maintenance cost of a DIY Google Flights scraper at scale is real.
- TorchProxies Plan X covers all the target markets in this guide: US, UK, India, Japan, South Korea, Germany, Canada, Indonesia, Netherlands, and Hong Kong. 120M+ IPs, city-level targeting, no rate limits, starting from $5/GB.
Scraping Google Flights is one of those tasks that looks simple until you actually try it. The first time I set this up, I got a seemingly working scraper in an hour, returning 200s, parsing HTML, printing results. Then I checked the actual data and found prices from three different countries for the same route. That's when I learned Google Flights personalizes results based on your IP's location, not just URL parameters. This guide covers the three working approaches with real Python code, why your proxy's country changes the prices you see, and how to detect silent failures before they corrupt your data.
Why Google Flights Requires a Different Approach
Before writing a single line of code, it's worth understanding what makes Google Flights harder to scrape than a regular webpage. Most guides skip this part and it's exactly why most scrapers built from those guides break quietly.
Google Flights is a JavaScript-rendered single-page application. When you send a plain HTTP request to https://www.google.com/travel/flights, the response you get back is essentially an empty shell. The actual flight data, prices, airline names, departure times, none of it is in that initial HTML. It all loads after JavaScript runs. This is why requests and BeautifulSoup alone are a dead end. You get HTML but not the HTML you need.
requests.get() responseThe second thing that makes Google Flights unique is the URL structure. Unlike Google Search which uses plain query strings like ?q=flights+new+york+to+london, Google Flights encodes your entire search into a single parameter called tfs. It looks like this:
https://www.google.com/travel/flights/search?tfs=CBwQAhoeEgoyMDI2LTA2LTAxagcIARIDSkZLcgcIARIDTEhS&hl=en
tfs URL parameter is a Base64-encoded Protocol Buffer (protobuf) string. Protobuf is Google's own binary serialization format, the same one they use internally across their systems. It encodes your entire flight search: departure airport, arrival airport, dates, trip type (one-way or round-trip), seat class, and passenger count. Decoding a tfs string reveals your search in readable form. The fast-flights library handles generating these strings for you so you don't need to understand protobuf to use it.The third thing, and the one that trips up the most scrapers, is location-based price personalization. Google Flights doesn't just use the gl URL parameter to determine which prices to show you. It also uses your IP address's geographic location. A search for JFK to LHR from a US residential IP will return different prices than the exact same search from a UK residential IP, even with identical URL parameters. The price difference can be meaningful. This matters especially if you're doing market research across multiple countries. Think of it like this: Google Flights is treating each IP origin as a different customer from a different market, because that's basically what it is.
What Data You Can Actually Extract
Google Flights is genuinely data-rich once you get through the rendering layer. Here's what's available and how reliably you can get each field.
| Data Field | What It Includes | Reliability | Notes |
|---|---|---|---|
| Flight Price | Ticket price in local currency for the IP's location | High | Always log the currency alongside the price. The same number in USD vs. EUR means very different things. |
| Airline Name | Operating carrier's name and IATA code | High | Codeshare flights sometimes show multiple airline names. Parse carefully. |
| Flight Number | Specific flight designation (e.g., BA117) | Medium | Not always visible in the initial results view. May require expanding a result. |
| Departure / Arrival Times | Scheduled departure and arrival, including timezone | High | Always store the timezone. A 10:00 departure from JFK is not the same as 10:00 from LHR. |
| Duration | Total flight time including layovers | High | Format varies: "13 hr 45 min" or "13h 45m" depending on language setting. |
| Stops / Layovers | Number of stops and intermediate airport codes | High | Direct flights show "Nonstop." Layover airport IATA codes are in the expanded view. |
| Carbon Emissions | CO2 estimate in grams per passenger, vs. route average | Medium | Available for most routes. Useful for sustainability-focused tools or research. |
| Price Trend Label | "Typical," "Low," or "High" relative to historical prices | Medium | Google shows this label near the price. Useful for price tracker tools. |
| Airport Codes | Departure and arrival IATA codes | High | Three-letter codes: JFK, LHR, NRT, ICN, DEL, HKG, AMS, FRA, CGK, YYZ. |
One field I'm intentionally not covering in depth in this guide is booking tokens. Google embeds booking URLs that deep-link into airline or OTA booking pages. Extracting those is technically possible, but they expire quickly and the legal picture around redirecting users through those links for commercial purposes gets more complicated. That deserves its own guide.
This guide is for educational and informational purposes only and does not constitute legal advice. Web scraping involves complex and evolving legal considerations that vary by jurisdiction. Before implementing any scraping solution, consult qualified legal counsel for guidance specific to your situation.
- Google's Terms of Service explicitly prohibit automated access to Google Flights. Violating ToS can result in IP blocking, account suspension, and civil liability for breach of contract.
- DMCA Section 1201 may apply if any technical protection measures are circumvented. The legal landscape around scraping is actively evolving and uncertain.
- Commercial use carries significantly higher legal risk than personal or research use. If you are building a commercial product using flight data, use a licensed API such as Amadeus.
- Jurisdiction matters. Laws vary between countries. GDPR applies in the EU; other frameworks apply elsewhere. This guide addresses general technical concepts only.
Setup: What You Need Before You Start
Quick environment check before diving into the code. Python 3.9 or later is required for both approaches below. Run python --version to confirm. Then install the libraries for whichever approach you're going with.
For Approach 1: fast-flights
pip install fast-flights
That's all you need for Approach 1. No browser installation, no extra dependencies. The fast-flights library ships with everything it needs to build and decode protobuf URL parameters and parse the JSON data embedded in Google Flights' server-side rendered HTML.
For Approach 2: Playwright + Proxy
pip install playwright beautifulsoup4 httpx playwright install chromium
Approach 1: fast-flights Library (Quick Lookups, No Browser)
The fast-flights library is genuinely clever. Instead of launching a browser and waiting for JavaScript to render, it generates the correct protobuf-encoded tfs URL parameter, fetches the server-side rendered page directly via HTTP, and extracts flight data from a JSON object embedded in a <script> tag. No Playwright, no browser overhead, no Chromium binaries to manage.
This is what I'd reach for first for any lookup that doesn't need to run at high frequency. It's fast and the API is clean.
Basic One-Way Flight Lookup
from fast_flights import FlightData, Passengers, Result, get_flights # Define your search result: Result = get_flights( flight_data=[ FlightData( date="2026-06-15", # YYYY-MM-DD format from_airport="JFK", # IATA departure code to_airport="LHR", # IATA arrival code ) ], trip="one-way", seat="economy", # economy, premium-economy, business, first passengers=Passengers( adults=1, children=0, infants_in_seat=0, infants_on_lap=0, ), fetch_mode="fallback", # Use fallback if primary parse fails ) # Check if the price is low, typical, or high vs. historical data print(f"Price trend: {result.current_price}") # Loop through flight results for flight in result.flights: print(f"Airline: {flight.name}") print(f"Departure: {flight.departure}") print(f"Arrival: {flight.arrival}") print(f"Duration: {flight.duration}") print(f"Stops: {flight.stops}") print(f"Is best: {flight.is_best}") print("---")
Round-Trip Search
from fast_flights import FlightData, Passengers, Result, get_flights result: Result = get_flights( flight_data=[ FlightData( date="2026-06-15", from_airport="JFK", to_airport="NRT", # Tokyo Narita ), FlightData( date="2026-06-29", # Return leg: add a second FlightData from_airport="NRT", to_airport="JFK", ), ], trip="round-trip", seat="economy", passengers=Passengers(adults=1, children=0, infants_in_seat=0, infants_on_lap=0), fetch_mode="fallback", ) print(f"Found {len(result.flights)} results") print(f"Price trend: {result.current_price}") for f in result.flights[:5]: # First 5 results print(f"{f.name} | {f.departure} -> {f.arrival} | {f.duration} | {f.stops} stops | Best: {f.is_best}")
trip="multi-city". At high request volumes without a proxy, you will hit Google's rate limits. The library does not handle proxy rotation internally. Some small regional airports return empty results because Google computes their data on-demand and the library's protobuf encoding may not trigger that computation.
What I'd use this for: building a personal price alert script that runs twice a day, doing one-off route research, or prototyping before deciding whether to build something bigger. What I wouldn't use it for: monitoring hundreds of routes hourly, or any use case where a failed request silently returning no data is a problem. More on that in the mistakes section.
Approach 2: Playwright + Rotating Residential Proxies (Scalable)
When fast-flights is not the right fit, Playwright is a more capable option. It launches a real Chromium browser and handles all the JavaScript rendering Google Flights requires. With a residential proxy attached, requests originate from a real residential IP address, which also gives you geo-accurate pricing for each target market.
The setup here uses TorchProxies rotating residential proxies. One new browser context per request ensures each search gets a fresh IP assignment. This is the configuration that actually holds up under sustained use.
Installing and Configuring the Scraper
import asyncio import json import random from datetime import datetime, timezone from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError # TorchProxies rotating residential credentials PROXY_HOST = "residential.torchproxies.com" PROXY_PORT = "31112" PROXY_USER = "your_username" PROXY_PASS = "your_password" COUNTRY_TIMEZONES = { "us": "America/New_York", "gb": "Europe/London", "de": "Europe/Berlin", "jp": "Asia/Tokyo", "kr": "Asia/Seoul", "in": "Asia/Kolkata", "id": "Asia/Jakarta", "nl": "Europe/Amsterdam", "hk": "Asia/Hong_Kong", "ca": "America/Toronto", } def is_blocked_or_empty(html: str, flights: list) -> bool: block_signals = [ "Our systems have detected unusual traffic", "g-recaptcha", "sorry.google.com", ] if any(s in html for s in block_signals): return True return len(flights) == 0 async def parse_flights_from_dom(page) -> list: """ Prefer ARIA/text-based locators over brittle class names. This still may need maintenance when Google changes UI. """ flights = [] cards = page.locator("li").filter(has_text="Select flight") count = await cards.count() for i in range(min(count, 20)): card = cards.nth(i) try: text = await card.inner_text(timeout=1000) flights.append({ "raw_text": text, "scraped_at": datetime.now(timezone.utc).isoformat(), }) except Exception: continue return flights async def scrape_google_flights( from_airport: str, to_airport: str, departure_date_iso: str, # YYYY-MM-DD country_code: str = "us", lang: str = "en", max_retries: int = 3, ) -> list: """ Note: Google Flights UI is dynamic and localized. Validate selectors in your target locales before scaling. """ results = [] url = f"https://www.google.com/travel/flights?hl={lang}&gl={country_code}" async with async_playwright() as pw: browser = await pw.chromium.launch( headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"], ) for attempt in range(1, max_retries + 1): context = await browser.new_context( proxy={ "server": f"http://{PROXY_HOST}:{PROXY_PORT}", "username": PROXY_USER, "password": PROXY_PASS, }, locale=f"{lang}-{country_code.upper()}", timezone_id=COUNTRY_TIMEZONES.get(country_code, "America/New_York"), ) await context.add_init_script( "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" ) page = await context.new_page() try: await page.goto(url, wait_until="domcontentloaded", timeout=45000) # Use role/name locators (safer than fragile CSS class selectors) await page.get_by_role("button", name="Where from?").click(timeout=5000) await page.keyboard.press("Control+A") await page.keyboard.type(from_airport, delay=60) await page.keyboard.press("Enter") await page.get_by_role("button", name="Where to?").click(timeout=5000) await page.keyboard.type(to_airport, delay=60) await page.keyboard.press("Enter") # ISO date (YYYY-MM-DD) is locale-independent await page.get_by_role("button", name="Departure").click(timeout=5000) await page.keyboard.type(departure_date_iso, delay=50) await page.keyboard.press("Enter") await page.wait_for_timeout(random.randint(2500, 4500)) html = await page.content() flights = await parse_flights_from_dom(page) if is_blocked_or_empty(html, flights): await asyncio.sleep(random.uniform(3, 7)) continue results = flights break except PlaywrightTimeoutError: await asyncio.sleep(random.uniform(2, 5)) except Exception: await asyncio.sleep(random.uniform(2, 5)) finally: await context.close() await browser.close() return results if __name__ == "__main__": data = asyncio.run( scrape_google_flights( from_airport="JFK", to_airport="LHR", departure_date_iso="2026-06-15", country_code="us", lang="en", ) ) print(json.dumps(data[:3], indent=2)) print(f"Total parsed cards: {len(data)}")
A few things worth flagging in this code. The is_blocked_or_empty() function is the most important part. Google Flights will sometimes return a valid-looking page with no actual flight data in it when it detects bot-like behavior. Without this check, your scraper would report "0 results" and move on, which corrupts your dataset silently. Always validate that you got actual data back, not just a successful HTTP response.
Timezone matching in the browser context matters for accurate results. If your TorchProxies IP is from Japan but your browser context reports a New York timezone, Google Flights may return mismatched pricing or localized content. The COUNTRY_TIMEZONES mapping keeps the browser locale and timezone consistent with the proxy country.
Geo-Targeting: Why Your Proxy Country Changes the Price You See
This is the section most other guides skip entirely. It's also the part that matters most if you're doing any kind of multi-market research.
Flight prices on Google Flights are not static values that you simply retrieve. They are personalized based on the geographic origin of the request. This happens for two reasons. First, airlines and travel booking systems practice market-based dynamic pricing. The same seat on the same flight is often listed at different prices in different regional markets based on local demand, purchasing power, and competitive landscape. Second, Google itself personalizes the displayed prices and currency based on the IP's location, separate from the gl URL parameter.
What this means in practice: if you're researching flight prices as a US-based traveler would see them, you need a US residential proxy IP, not just ?gl=us in the URL. Both are needed. The parameter tells Google which regional results to prioritize. The proxy IP tells Google (and the underlying booking systems) which market's pricing to display.
| Target Market | Country Code | Language (hl) | Proxy Country Needed | Notes |
|---|---|---|---|---|
| United States | us |
en |
US residential IP | Prices in USD. City-level targeting useful for US-originating searches. |
| United Kingdom | gb |
en |
UK residential IP | Prices in GBP. Note: the country code is gb, not uk, in most URL contexts. |
| Canada | ca |
en or fr |
CA residential IP | Prices in CAD. Use hl=fr for Quebec-targeted research. |
| Hong Kong | hk |
zh-TW |
HK residential IP | Traditional Chinese. Different results from mainland China. Prices in HKD. |
| India | in |
en |
IN residential IP | Prices in INR. Google holds over 97% search share in India as of 2025. |
| Germany | de |
de |
DE residential IP | Prices in EUR. German language results show meaningfully different route priorities. |
| Indonesia | id |
id |
ID residential IP | Prices in IDR. Regional Asian carriers prominently featured. |
| Netherlands | nl |
nl |
NL residential IP | Prices in EUR. KLM routes feature prominently from AMS-origin searches. |
| South Korea | kr |
ko |
KR residential IP | Prices in KRW. Korean Air and Asiana prominently featured from ICN. |
| Japan | jp |
ja |
JP residential IP | Prices in JPY. ANA and JAL featured from NRT/HND searches. |
The key takeaway from this table: both the gl URL parameter and a matching residential proxy IP from the target country are needed for geo-accurate results. One without the other will give you mismatched pricing.
Building a Basic Flight Price Tracker
Most guides stop at "here's how to get data once." The actually useful version of this tool is one that runs on a schedule, saves historical prices, and alerts you when something drops. Here's a minimal working version using fast-flights plus a simple JSON file as the data store.
import json import os import re from datetime import datetime from fast_flights import FlightData, Passengers, get_flights HISTORY_FILE = "flight_prices.json" DROP_THRESHOLD_PCT = 10.0 # Alert if price drops >= 10% def load_history() -> dict: if os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} def save_history(history: dict) -> None: with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(history, f, indent=2, ensure_ascii=False) def parse_price(value) -> float: """ Handles ints/floats or strings like '$823', 'EUR 1,245', 'JPY 42,000' """ if value is None: return 0.0 if isinstance(value, (int, float)): return float(value) s = str(value) cleaned = re.sub(r"[^\d.,]", "", s).replace(",", "") try: return float(cleaned) if cleaned else 0.0 except ValueError: return 0.0 def check_route(from_airport: str, to_airport: str, departure_date: str, route_key=None): key = route_key or f"{from_airport}-{to_airport}-{departure_date}" try: result = get_flights( flight_data=[ FlightData( date=departure_date, from_airport=from_airport, to_airport=to_airport, ) ], trip="one-way", seat="economy", passengers=Passengers(adults=1, children=0, infants_in_seat=0, infants_on_lap=0), fetch_mode="fallback", ) except Exception as e: print(f"Error fetching {key}: {e}") return if not result.flights: print(f"{key}: No results (possible block or no inventory).") return # Filter to flights with a parseable numeric price priced = [] for f in result.flights: p = parse_price(getattr(f, "price", None)) if p > 0: priced.append((f, p)) if not priced: print(f"{key}: Flights returned but no parseable numeric price.") return best_flight, best_price = min(priced, key=lambda x: x[1]) timestamp = datetime.now().isoformat() entry = { "timestamp": timestamp, "airline": best_flight.name, "departure": best_flight.departure, "arrival": best_flight.arrival, "duration": best_flight.duration, "stops": best_flight.stops, "price": best_price, "price_raw": str(getattr(best_flight, "price", "")), "price_trend": getattr(result, "current_price", None), "is_best": bool(getattr(best_flight, "is_best", False)), } history = load_history() history.setdefault(key, []) previous_price = history[key][-1]["price"] if history[key] else None history[key].append(entry) save_history(history) print(f"{key}: {best_flight.name} | {best_price:.2f} | trend={entry['price_trend']}") # Real numeric drop detection if previous_price and previous_price > 0: drop_pct = ((previous_price - best_price) / previous_price) * 100 if drop_pct >= DROP_THRESHOLD_PCT: print( f"ALERT: Price dropped {drop_pct:.1f}% " f"({previous_price:.2f} -> {best_price:.2f}) for {key}" ) if __name__ == "__main__": routes = [ ("JFK", "LHR", "2026-06-15"), ("LHR", "NRT", "2026-07-01"), ("AMS", "HKG", "2026-08-10"), ] for dep, arr, date in routes: check_route(dep, arr, date)
To make this run on a schedule, set up a cron job. On a Linux server or Mac:
# Add to crontab (run: crontab -e)
0 */6 * * * /usr/bin/python3 /your/path/flight_tracker.py >> /your/path/tracker.log 2>&1
Note on pricing data: depending on your installed version and target route, fast-flights may return a trend label ("low," "typical," "high") rather than a numeric price. The Block 7 code above handles this by filtering for flights where a parseable numeric price is available. If no numeric price is returned for your route, the Playwright approach will give you the actual displayed fare.
Mistakes I Made (And You Will Too)
These are the ones that cost me the most time. Listed in order of how much they stung.
len(flights) > 0 before treating a result as valid data..pIav2d container class has changed before and will change again. Always build selector fallback chains, not single-point dependencies.context for each search request.When to Skip DIY Scraping Entirely
This section is the one I actually want people to read before deciding to build a scraper, because sometimes the right answer is to not build it at all.
You probably do not need to build your own Google Flights scraper if:
- You're checking prices for your own travel planning. Just use Google Flights directly in your browser. This guide is overkill for personal use.
- You're building a commercial OTA or booking platform. Use Amadeus API. It is a licensed data feed with SLAs, legal clearance, and support, though production usage is pay-as-you-go and costs can add up at scale. Note: Skyscanner's API requires partner application and approval and is not available for general developer use. A DIY Google Flights scraper at commercial booking scale is a maintenance concern and carries real legal risk under Google's Terms of Service.
- You need guaranteed real-time booking availability. Google Flights data is great for prices and schedules but it's not the authoritative source for seat inventory at the time of booking. Airlines' own APIs are the truth source for that.
- Your team doesn't have bandwidth to maintain a scraper. Google Flights changes its HTML structure regularly. Someone needs to own that maintenance. If that person doesn't exist on your team, budget for a SERP API instead.
If your use case is mainly checking one route for personal travel, you genuinely do not need this. Google Flights works fine for that. The setup in this guide makes sense when you're querying dozens of routes across multiple markets on a recurring schedule.
The Bottom Line
Google Flights is scrapable in 2026. It's not easy, but it's doable with the right setup. The three approaches here cover different points on the speed/reliability/maintenance tradeoff curve: fast-flights for quick lookups without a browser, Playwright with residential proxies for reliable production scraping, and SERP APIs when the maintenance overhead isn't worth it at scale.
The one thing most guides miss is the country-based pricing piece. Get that wrong and your data is accurate-looking but meaningfully wrong. Your proxy country needs to match the market you're researching. That's the most important detail in this entire guide.
len(flights) > 0.
FAQs
pip install fast-flights then use the get_flights() function with IATA codes and dates. No browser needed. For more robust data extraction: Playwright with a rotating residential proxy handles JavaScript rendering and returns geo-accurate prices per target market. For commercial use where legal compliance matters, use a licensed API such as Amadeus rather than scraping.
tfs parameter is a Base64-encoded Protocol Buffer (protobuf) string that encodes your entire search query: departure and arrival airport codes, date, trip type, seat class, and passenger count. Google uses protobuf for efficiency instead of plain query parameters. The fast-flights library generates and decodes these strings automatically. You pass IATA codes and dates, it handles the encoding.
gl URL parameter. Both the parameter and a country-matching proxy IP are needed for geo-accurate pricing data.