When I plan a trip I rarely know the exact dates. I know something like: a week in Barcelona sometime in May, out in the morning, back in the evening, one stop at most. Every flight search site wants two exact dates, so I end up running the same search for twenty different days and comparing the results in a spreadsheet.
So we built SkySearch, a free tool that takes the loose question directly: a route, a period, a minimum and maximum stay, departure-time windows for each direction and a maximum number of stops. It checks every date combination and recommends the trip that fits best, not just the cheapest one.
This post is about the part that turned out to be the real problem: the API budget.
The numbers
Our flight data comes from Duffel. Like most flight APIs, it has no flexible-date search: every request is one exact pair of dates. And the account is limited to 30 offer requests per minute.
A month-long period with a 5-night minimum stay has 351 possible (outbound, return) date pairs. Pricing each one as a round trip would take almost 12 minutes of pure waiting, before parsing a single response. Nobody waits 12 minutes for a search.
Scan first, then price
The trick is to stop treating every date pair as a separate question.
- Scan one-way fares day by day. One one-way search per possible outbound date and one per possible return date. For a month that is about 2 calls per day, roughly 60 calls instead of 351.
- Combine the cheapest valid legs into estimated trips. Every outbound day can pair with every return day that gives a stay inside the user's range. The filters (time windows, stops, max flight time) are applied locally.
- Score the estimates and keep the top candidates. Only the best few date pairs (8 by default) go on to the next step.
- Price those as real round trips. One-way sums are only estimates, since round-trip fares are often different. So every result the user sees is a genuine round-trip offer.
A month-long search ends up at about 70 requests, which is 2–3 minutes with the rate limit. When the number of pairs is small (a short period, or a narrow stay range), the scan would cost more than it saves, so we skip it and price every pair directly:
const scanCost = outDates.length + inDates.length;
const priceDirectly = pairs.length <= Math.min(budget, scanCost + config.roundTripCandidates);
We also changed the default period from a whole month to the first half of next month. With a 5–8 night stay that is about 28 requests, which fits inside a single rate-limit window and finishes in under a minute.
Waiting instead of failing
Duffel reports ratelimit-remaining and ratelimit-reset headers. The client reads them and, when the window is used up, waits for the next one instead of throwing a 429 at the user. The UI streams progress events (NDJSON) the whole time, so the user watches a map fill up with the routes the airlines have returned so far instead of staring at a spinner.
Caching the scan separately
The per-day scan does not send time windows or max_connections to the API. It filters only locally. That sounds wasteful, but it means the scan answer does not depend on the user's filters. If someone repeats the search with a different stay length, different departure times or fewer stops, the cached scan is reused and only the final round-trip pricing calls are made.
Answers are cached in isolate memory (with in-flight de-duplication) and in Cloudflare's Cache API, so other isolates in the same data centre reuse them too: 10 minutes for prices shown to the user, 30 minutes for scan answers.
Scoring "best", not "cheapest"
Every valid trip gets a score from 0 to 100. Price counts for 40%. Matching the preferred departure times counts for another 40%, split between the two directions. Total travel time and the number of stops make up the rest. A slightly more expensive direct flight at 9 a.m. can beat a cheaper one that leaves at 5 a.m. with a long layover.
The top result comes with a plain-language explanation ("direct both ways, morning departure, afternoon return, 10 nights, lowest price among similarly convenient options"). There is no LLM involved: the bullets are generated from the flight data, so they cannot invent anything.
A side effect of the scan: we already know the cheapest one-way sums for stay lengths outside the user's range. When a 7-night trip would be noticeably cheaper than the 10 nights they asked for, SkySearch says so, at no extra API cost.
Things that bit us
- Time zones. The API returns airport-local wall-clock times plus an IANA zone. Departure windows compare the local time of day; durations convert both ends to UTC. Stay length counts nights from the local arrival date to the local departure date.
- Stops. A "direct" flight can have a technical stop. We count stops from segments, connections and technical stops alike.
- Cloudflare Workers limits. A month-long search makes about 70 outbound requests and parses large JSON responses, which is over the free plan's 50 subrequests and 10 ms of CPU per request. It runs on the paid plan.
Stack
Next.js 16 (App Router) deployed to Cloudflare Workers via OpenNext, Duffel behind a small FlightProvider interface (so another source can be added without touching filtering, scoring or the UI), Cloudflare Turnstile in front of the search because each search costs real API calls, and Vitest for the framework-free search logic.
Try it
SkySearch is free and needs no sign-up: https://skysearch.itfriends.ai/
I'd love feedback, especially on two things: would you use "search a period, not dates" for your own trips, and does the score make sense to you, or would you rather just sort by price?
Top comments (0)