DEV Community

Matvey
Matvey

Posted on

46 Runs, One Route, the Same Cheapest Flight Every Time. The Two Sources Never Once Agreed on Its Price

I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them does something that sounds trivial and is not: it runs the same flight search on Google Flights and KAYAK at once, puts both answers in one dataset, and adds a row saying which source was cheaper and by how much.

That verdict row is the product. It is also the part I trusted least, so I let my own health check hammer it: the same input every day since 6 October — DPS → SIN, one way, 15 March 2027, economy, 30 rows per source. 46 successful runs between 6 October and 6 November. This is what a month of that looks like.

Both sources agree on the flight. Neither agrees on the price

In 46 of 46 runs the cheapest row from Google Flights and the cheapest row from KAYAK are the same physical flight: Jetstar JQ86, departing 14:55, landing 18:00, A321neo. No matching heuristics needed — same flight number, same minute.

In 46 of 46 runs the two prices differ. Not once in a month did they land on the same number for that seat.

The asymmetry in how they differ is the interesting part. Google's price for JQ86 across the month took three distinct values:

Google Flights, JQ86 runs
$60 23
$59 1
$65 22

One step, on 27 October, between a run at 00:10 and a run at 20:07. Otherwise flat for three weeks at a time.

KAYAK's price for the same flight, same runs, took eleven distinct values between $57 and $76 — a 33% spread — and moved inside the hour:

20:07 20:09
27 Oct $67 $63
20:10 20:27
31 Oct $61 $68
20:05 20:11
5 Nov $68 $61

So the verdict flipped: Google cheaper in 25 runs, KAYAK in 21. A coin toss, and the coin is KAYAK's, because Google's side of it barely moved.

The seller column explains all of it

KAYAK is a metasearch engine, so its price is not the airline's price — it is whichever agency KAYAK had surfaced at the moment the run stopped reading. The row says who:

seller on the JQ86 row prices seen
Jetstar (direct) $57, $61, $63
Booking.com $62, $63, $67, $68
Gotogate $68
Kiwi.com $69, $74, $76
CheapOair $73
Justfly.com $75, $76
Priceline $79, $83, $84, $85

Google's rows carry no seller at all — seller was null on all 29 Google rows of the latest run, because Google Flights hands off to the airline or an agency after the click rather than quoting one on the results page.

Part of KAYAK's spread is my own fault, and I wrote that up separately: KAYAK's search is a poll that fills in with sellers while you read it, and a row cap that is satisfied early stops the read early, so price is the minimum of a partially arrived set. With the cap fixed at 30 for all 46 runs, the remaining movement is sellers rotating in and out of the cheapest slot.

Either way the practical rule is the same, and it is not "KAYAK is dearer":

The gap between two metasearch engines is mostly a seller-rotation gap, not a fare gap. A single run tells you which source was cheaper in that minute. If you are publishing "source X is 8% cheaper on this route", you need a schedule, not a run.

And across the rest of the board, the direction reverses

Match every flight that both sources returned in the same run, on flight numbers plus departure minute, taking each source's cheapest row per flight. Excluding JQ86, that is 530 matched pairs over the month:

  • KAYAK cheaper on 443
  • Google cheaper on 26
  • identical on 61 (11.5%)
  • median gap −3.1% in KAYAK's favour

So KAYAK is systematically a few percent cheaper on the body of the result set, and still loses the headline on the single cheapest flight more than half the time. If your pipeline reads only the floor — the cheapest row, the verdict's cheaperSource — you are sampling the one row in the dataset with the most variance in it. Cheap insurance: compute your own median over matched flights alongside the verdict.

Three joins that look right and are not

Join on flight numbers and the departure minute, nothing else. Not on aircraft: KL836 in the latest run is Boeing 777 to Google and Boeing 777-300ER to KAYAK, same seat, same price. Not on departAt as a string either — Google sends 2027-03-15T14:55, KAYAK sends 2027-03-15T14:55:00. Slice to 16 characters first. (The Actor's own merge mode uses exactly that key.)

Expect the same flight twice from KAYAK. Over the 46 runs KAYAK sent 43 duplicate rows — same flight, different seller, different price — and Google sent none. Today's control run has JQ86 twice: $61 from Jetstar and $67 from Booking.com. Deduplicate keeping the minimum, not the first row. And do not use sellersForThisFlight as a seller count while you do it: on those duplicate rows it reads 1.

Codeshares do not merge, by design. QF5659 appears in KAYAK's rows in all 46 runs and in Google's in none — because Google folds it into JQ86's legs[].operatedBy: "Qantas QF5659". Same A321neo, same 14:55 departure. As QF5659 it was priced $83 to $116; as JQ86, $61 to $68. A "cheapest flight per aircraft" report has to resolve that itself; no source will do it for you.

One more thing worth knowing before you write a dashboard: the two sources are not looking at the same shelf. Per run the overlap between Google's ~28 flights and KAYAK's ~29 was on average 12.5 flights, ranging 8 to 16. Each source's top-30 is its own window on the day, ranked by its own idea of "best".

The merge mode, and where it helps

Set outputMode to byFlight and one row carries every source's price for that flight. LHR → CDG on 4 December, Google Flights plus Momondo, 8 rows each → 12 flight rows, 4 of them merged:

{
  "type": "flight",
  "airline": "British Airways", "flightNumbers": ["BA310"],
  "departAt": "2026-12-04T13:20",
  "price": 111, "currency": "EUR", "cheapestSource": "google-flights",
  "priceBySource": { "google-flights": 111, "momondo": 132 },
  "sellerBySource": { "google-flights": null, "momondo": "eDreams" },
  "priceGap": 21
}
Enter fullscreen mode Exit fullscreen mode

Momondo priced all four shared BA flights at exactly €132 against Google's €111 — a flat €21, which is a seller margin, not four independent fares. The other 8 of 12 rows came from one source only.

Calling it

curl -X POST "https://api.apify.com/v2/acts/lergassy~flight-price-comparison/run-sync-get-dataset-items?token=APIFY_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"origin":"DPS","destination":"SIN","departureDate":"2027-03-15",
       "sources":["google-flights","kayak"],"maxResultsPerSource":5,"currency":"USD"}'
Enter fullscreen mode Exit fullscreen mode

Today's control run of exactly that, from a clean account: 28.6 seconds, 9 flight rows plus a free verdict row. The verdict read {"google-flights": 65, "kayak": 61} — KAYAK cheaper, the other way round from yesterday's run, which is the whole point of this article.

Cost: the parent run charged $0, and the two source Actors it started on the account charged $0.005 and $0.011. That is how Flight Price Comparison is wired — each source is its own Actor run on your own account, billing its own rows, with no comparison fee on top. The side effect is a concurrency ceiling: four sources plus the parent is five concurrent runs, the entire Apify Free-plan allowance, so the Actor retries a source that gets refused (8 attempts, 15 seconds apart) instead of dropping it. Run times over the 46 health checks: median 40 s, range 12–126 s.

A failing source writes a free error row and the others still return, so a run is never all-or-nothing.

When this is the wrong tool

It needs an origin, a destination and a date — there is no browse mode, so "cheapest week in March from Bali" is a different job. Fares are point-of-sale dependent, and these reads are anonymous and currency-forced, so your own booking screen may differ. Round-trip rows are the total for both directions, and departAt/arriveAt span the whole trip — subtract them and you get a holiday, not a flight; use legs or the outbound*/inbound* columns.

And treat gapPercent with care when the sources are shaped differently. On a STN → DUB test the verdict reported Ryanair cheaper by 177% — true in that both numbers are the cheapest row of their source, misleading in that Ryanair's row is the cheapest one-way fare of the day excluding bags while KAYAK's was a priced round trip for two adults. The verdict compares the cheapest rows it was given. It does not promise they are the same product; on a single-source pair like Google plus KAYAK they usually are, and I checked that 46 times before saying so.

Top comments (0)