How to find the cheapest day to fly
Google Flights answers "which day is cheapest" with a calendar you read by eye and cannot export. The same answer as rows — one per departure day, ranked, with the airline and duration attached — is a single run, and once it is scheduled you also have the price history Google keeps to itself.
The short version
- Name the route and the first departure day. Airport codes, and a departure date that can be relative — "30 days" keeps a scheduled run at a fixed lead time forever.
- Set the window length. Every day in the window is its own search. Seven days is seven searches; a month is thirty. Sixty days is the cap per run.
- Let it keep the cheapest fare per day. One row per day: the cheapest itinerary Google lists for that day under your cabin and stop filters, with how many itineraries it beat.
- Read rank_in_window. Rank 1 is the cheapest day. The ranking is computed over the whole window before any price filter, so a filtered run still tells you where the day you kept sits.
One row per day is the whole idea
The question is rarely "what is the fare". It is "which day should I leave", and that is a ranking over a window.
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/flight-price-tracker").call(run_input={
"routes": ["LIS-LHR"],
"departDate": "30 days", # relative: always 30 days out
"days": 14, # price each of the next 14 departure days
"currency": "EUR",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["type"] == "fare":
print(row["rank_in_window"], row["depart_date"], row["price"],
row["airline"], f'{row["stops"]} stops', row["duration"])
Fourteen days is fourteen rows. The one with cheapest_in_window: true is the answer; rank_in_window orders the rest, so "the three cheapest departures in October" is a filter on one column rather than a spreadsheet exercise.
{
"type": "fare", "route": "LIS-LHR", "origin": "LIS", "destination": "LHR", "trip": "one_way",
"depart_date": "2026-10-05", "seat": "economy", "adults": 1, "currency": "USD",
"price": 127, "round_trip_total": false,
"airline": "Tap Air Portugal", "stops": 0,
"depart_time": "8:00 PM", "arrive_time": "10:55 PM",
"duration": "2 hr 55 min", "duration_minutes": 175,
"co2_kg": 123,
"itineraries_seen": 12, "cheapest_in_window": true, "rank_in_window": 1
}
itineraries_seen: 12 is the row's own audit trail: this fare was the best of twelve Google listed that day. A day with two itineraries and a day with forty are different kinds of cheap.
The cheap day is not always the cheap trip
Two columns stop you from booking a bad answer.
stops — the cheapest fare on a day is often the one with a connection. Filter to nonstop before ranking if a connection is not acceptable, and the ranking re-forms over the fares you would actually buy.
duration_minutes — a fare that is 8% cheaper and four hours longer is a different product. Sorting by price alone hides that; having both on the row means you can sort by whatever you actually optimise.
And if you want every itinerary on a day rather than the cheapest one — to compare airlines head to head, or to see the second-cheapest option — that is the Google Flights scraper instead, which bills per itinerary.
The history you cannot get any other way
Google's own price tracking emails you when a fare moves, on the routes it chooses, and keeps the series. Running this on a schedule gives you the series.
Every row carries fetched_at, so a daily run over a 30-day window is one point per route per departure day per day — a proper surface, not a snapshot. Two things fall out of it that a single search cannot answer:
- How a specific departure day drifts as it approaches. Group by
depart_dateand plotpriceoverfetched_at. That is a booking curve for your route. - Whether today's cheap is actually cheap. Comparing today's rank-1 price with the same window a week ago is the difference between "this is the cheapest day in the window" and "this is a genuinely good fare".
Because the departure date is relative, the window moves with the calendar and never prices a day that has passed.
Alerts that cost nothing when nothing happens
A price ceiling filters days over the line before billing:
{"routes": ["LIS-LHR", "OPO-LGW"], "departDate": "14 days", "days": 30,
"maxPrice": 90, "currency": "EUR", "maxStops": 0}
A run that finds no qualifying day delivers no rows and costs nothing, while the status row still reports how many days were filtered and what the window's cheapest fare was — so a quiet alert still tells you how far off the line you are. That is the whole cost model of an alert done properly: you pay when there is something to say.
The honest limitations
- Google's list is what you get. Fares that appear only after login, in corporate channels, or on airline sites Google does not index are not in it.
- Prices include what Google shows — taxes and carrier fees as presented. Baggage and seat fees are not in the number.
- A currency swap fails the run rather than delivering rows priced in something you did not ask for. Some markets ignore a requested currency, and a silently converted number is worse than an error.
- The window is capped at 60 days per run. Run twice for a longer horizon.
- Google renders every itinerary twice on its own page. Rows are deduplicated before billing, so you are never charged for the site's markup — worth checking in any flight scraper you evaluate, because the duplicate is easy to bill and hard to notice.
Run it
Flight Price Tracker — $0.004 per delivered fare row, one per day, with status rows, filtered days and empty routes free. For every itinerary rather than the daily cheapest, Google Flights Prices at $0.005 per itinerary. Related: scheduling with relative dates so the window never goes stale.
FAQ
Is there a Google Flights API?
No public one — Google retired QPX Express. The alternatives are aggregator APIs (Amadeus, Duffel, Kiwi) that sell fares from their own inventory and generally need a commercial agreement, and SERP resellers that charge per search whether or not it returned anything. Reading Google Flights directly and billing per delivered row means an empty route costs nothing.
Are round-trip prices per leg or for the whole trip?
The whole trip, as Google shows them. Set a trip length and every departure day becomes a round trip returning that many days later, with the price being the total for both legs and a round_trip_total flag on the row so the two can never be mixed up in a table.
Why are the results only in English?
Because the parser reads Google's own English accessibility labels, which is the most stable anchor on the page. Language does not change fares; currency and market do, and both are inputs.
What happens when two days share the lowest fare?
The earlier departure takes rank 1. It is an arbitrary rule, but it is a stated one, so a tie does not silently reorder between runs.
Run Flight Price Tracker on Apify →
Other actors used on this page: apify.com/kestrel/google-flights-prices.