Forecasting Keyword Seasonality 90 Days Out
Seasonal content published during the peak is content published too late. Here is how to read a seasonality curve, the lead-time maths between publishing and ranking, and a Python script that turns 24 months of Google Trends data into publish dates.
Table of contents
Short answer: work backwards from the month demand starts rising, not the month it peaks, and subtract your crawl lag plus your ranking lag — for most sites that means publishing roughly 11 weeks early, and briefing two weeks before that. Pull 24 months of Google Trends data for the query, fold it into a 12-month index, find the first month that reaches 50% of peak interest, then subtract the time it takes Google to crawl the page and the time it takes to climb into a clickable position. Publish before that date. The script at the bottom of this post does the arithmetic.
The reason this page exists
Five queries about search seasonality have shown up in my Search Console over the last 85 days: keyword seasonality (42 impressions, average position 79.5), seasonal seo (25 impressions at 84.5), seasonal keyword trends (16 at 36.6), seo seasonality at 64.8, and — my favourite — someone searching for the search volume of "fall gutter cleaning", where I sit at position 8.0.
Google was matching all of that to a post about fashion content marketing, sitting at position 53.9. That is not a ranking problem I can fix with better headings. There was no page on this site about forecasting seasonal demand, so Google picked the least-wrong thing it had. This is the page.
I use gutter cleaning as the worked example throughout, because somebody is literally searching for its volume and because it is a textbook curve: nobody thinks about their gutters until leaves are in them.
Reading a seasonality curve
Google Trends gives you a line. Three things about that line matter, and the peak is the least important of them.
Amplitude tells you whether to bother
Divide the highest month by the lowest month. If the ratio is under about 1.5x you are looking at noise dressed up as a season, and a dedicated seasonal page is not worth the effort — write the evergreen version instead. In my gutter example below the ratio is 6.3x, which is a real season.
Onset is the deadline, peak is the exam
The mistake I keep seeing is teams treating the peak month as the target. By the time the peak arrives the SERP has already been decided. What you actually need is the onset — the point where the curve starts climbing hard, which I define as the first month on the rise that reaches 50% of the peak value. Your page needs to be indexed and settled before onset, because that is when Google is re-evaluating which pages deserve the seasonal SERP.
The noise floor
Google's own FAQ about Google Trends data says the platform incorporates "statistical noise that includes small and random fluctuations that don't represent actual search behavior", and that this is most visible on low-volume queries. If your curve looks jagged rather than smooth, you are probably reading noise. Widen the query, widen the region, or use a topic rather than a search term.
What Google Trends can and cannot tell you
Trends is the right free source for the shape of demand and the wrong source for its size. Per Google's documentation, each data point "is divided by the total searches of the geography and time range it represents", then scaled 0-100 against the topic's own maximum in that window. A 100 in October means October was that query's biggest month — not that it beat any other query, and not that it was big in absolute terms. Google also states that Trends "is not a scientific poll" and is "not a perfect mirror of search activity", since it samples rather than counting everything and filters out duplicates and very low-volume searches.
Two consequences:
- —You cannot forecast traffic from Trends alone. For absolute-ish numbers you need Google Ads Keyword Planner, and even that reports rounded ranges rather than exact counts.
- —Your window changes your numbers. Pull 24 months and the scale is set by the biggest month in those 24; pull 5 years and every value shifts. Fix the window before building the index and reuse it every re-run.
The lead-time maths
This is where most seasonal calendars fall apart. People budget for writing time and forget that publishing is not the finish line.
| Stage | Budget I use | Where the number comes from |
|---|---|---|
| Brief and draft | 14 days | My own throughput. Yours will differ. |
| Crawl and index | 21 days | Google says crawling "can take anywhere from a few days to a few weeks" |
| Climb to a clickable position | 56 days | Placeholder. Replace with your measured time-to-first-click. |
| Total lead time | 91 days | Sum of the above |
The crawl figure is the one I can source: Google Search Central's page on asking Google to recrawl your URLs states that crawling can take anywhere from a few days to a few weeks, and that requesting indexing does not guarantee it.
The ranking figure is a judgement call, and I want to be honest that it is. The best public data I found is Ahrefs' May 2025 time-to-rank study. On their sample of 2 million URLs created in October 2023, restricted to non-empty English content, 6.11% reached the top 10 for any keyword within twelve months, and of the pages that did make it, 40.82% got there within the first month. So a two-month budget is optimistic-but-not-absurd for the pages that will rank at all — and irrelevant for the 94% that never do. Read that study yourself before adopting my 56 days: how long it takes to rank in Google.
Anyone quoting you a precise days-to-rank number is quoting a median from a sample that does not contain your domain. My own version is in how long SEO actually takes.
Why impressions before you rank are worthless
My Search Console makes this concrete. Over 85 days the United States sent 970 impressions at average position 38.8 and zero clicks. Nepal sent 74 impressions at position 9.2 and 8 clicks — a 10.8% CTR. Same site, same period. Position 38 during the peak earns nothing; it is a page that arrived late dressed as a page that is working. Which numbers I watch instead is in the five SEO metrics that matter.
The script
Stdlib Python, no dependencies. Feed it 24 months of Trends values, get back a seasonal index, a peak, an onset, and the dates you have to hit.
"""Turn 24 months of Google Trends interest into a publish calendar. Python 3, stdlib only."""
from datetime import date, timedelta
from statistics import fmean
# WORKED EXAMPLE, not measured data. Replace with your own Trends CSV export:
# (year, month, relative interest 0-100). Oldest month first, 24 rows.
SERIES = [
(2024, 1, 18), (2024, 2, 15), (2024, 3, 22), (2024, 4, 30),
(2024, 5, 28), (2024, 6, 26), (2024, 7, 31), (2024, 8, 47),
(2024, 9, 72), (2024, 10, 100), (2024, 11, 81), (2024, 12, 40),
(2025, 1, 20), (2025, 2, 16), (2025, 3, 24), (2025, 4, 33),
(2025, 5, 27), (2025, 6, 25), (2025, 7, 34), (2025, 8, 51),
(2025, 9, 76), (2025, 10, 96), (2025, 11, 78), (2025, 12, 38),
]
CRAWL_DAYS = 21 # Google: crawling can take "a few days to a few weeks"
RANK_DAYS = 56 # your own measured time-to-first-click; 8 weeks is a placeholder
ONSET_FRACTION = 0.50 # the season "starts" at 50% of peak interest
def seasonal_index(series):
"""Average each calendar month across all years, then scale so peak month = 1.0."""
by_month = {m: [] for m in range(1, 13)}
for _, month, value in series:
by_month[month].append(value)
means = {m: fmean(v) for m, v in by_month.items() if v}
peak = max(means.values())
return {m: v / peak for m, v in means.items()}
def onset_month(index):
"""First month of the rise into the peak that reaches ONSET_FRACTION of peak."""
peak_month = max(index, key=index.get)
month = peak_month
while True:
prev = 12 if month == 1 else month - 1
if index[prev] < ONSET_FRACTION or prev == peak_month:
return month, peak_month
month = prev
def publish_calendar(series, from_date):
index = seasonal_index(series)
onset, peak = onset_month(index)
lead = timedelta(days=CRAWL_DAYS + RANK_DAYS)
year = from_date.year
if date(year, onset, 1) - lead < from_date:
year += 1 # this season's window has closed; aim at the next one
onset_start = date(year, onset, 1)
return {
"index": index,
"onset_month": onset,
"peak_month": peak,
"publish_by": onset_start - lead,
"brief_by": onset_start - lead - timedelta(days=14), # writing time
"last_useful_update": date(year, peak, 1),
"amplitude": max(index.values()) / min(index.values()),
}
if __name__ == "__main__":
plan = publish_calendar(SERIES, date(2026, 3, 1))
names = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()
for m in range(1, 13):
bar = "#" * round(plan["index"][m] * 40)
print(f"{names[m - 1]} {plan['index'][m]:.2f} {bar}")
print()
print(f"peak month .......... {names[plan['peak_month'] - 1]}")
print(f"season onset ........ {names[plan['onset_month'] - 1]}")
print(f"peak-to-trough ratio {plan['amplitude']:.1f}x")
print(f"brief written by .... {plan['brief_by']}")
print(f"published by ........ {plan['publish_by']}")
print(f"last useful update .. {plan['last_useful_update']}")
assert plan["peak_month"] == 10
assert plan["publish_by"] < date(2026, 9, 1)
Output, run on 1 March 2026 with the illustrative gutter numbers above:
Jan 0.19 ########
Feb 0.16 ######
Mar 0.23 #########
Apr 0.32 #############
May 0.28 ###########
Jun 0.26 ##########
Jul 0.33 #############
Aug 0.50 ####################
Sep 0.76 ##############################
Oct 1.00 ########################################
Nov 0.81 ################################
Dec 0.40 ################
peak month .......... Oct
season onset ........ Aug
peak-to-trough ratio 6.3x
brief written by .... 2026-05-02
published by ........ 2026-05-16
last useful update .. 2026-10-01
To rank for fall gutter cleaning in October, the brief is written in early May. Not September. That gap between intuition and arithmetic is the entire point of the script.
The assertions at the bottom exist so the thing fails loudly if I break the month arithmetic while editing. The year += 1 branch is the one that matters in practice: run this in July and it tells you this year's window is shut, handing you next year's date instead of a date in the past.
Where I deliberately kept it dumb
No trend removal, no de-seasonalising, no confidence intervals. Two years of monthly data is 24 points, and fitting anything clever to 24 noisy points produces confident nonsense. If a query is growing year on year, put the two years side by side and decide with your eyes.
Running it as a calendar, not a one-off
The output above is one row of a calendar. Run the script over every seasonal query you care about, sort by publish date, and you have next year's editorial schedule ordered by deadline instead of by enthusiasm.
| Phase | Months (gutter example) | What ships |
|---|---|---|
| Dead season | Dec-Apr | Write and publish next season's page. Nothing to lose. |
| Pre-onset | May-Jul | Internal links pointed at it, schema, images. No rewrites. |
| Onset to peak | Aug-Oct | Freeze the page. Update prices and dates only. |
| Post-peak | Nov | Record actual clicks against forecast; adjust RANK_DAYS. |
That last row is the one people skip and the only one that improves the model. Your RANK_DAYS should be a measured number by year two.
Freezing during the season is a rule I learned the annoying way: substantive edits mid-season invite a re-crawl and re-evaluation exactly when you least want one. And none of this saves you if the page cannot be indexed at all — I lost three months to a hostname mistake, documented in the canonical hostname bug. A repeatable calendar is a system rather than a habit: what a content system actually is.
What I could not find evidence for
I looked for a controlled study measuring how much earlier a page must publish to win a seasonal SERP — publish dates varied, everything else held equal. I did not find one and do not think one exists publicly. Everything above is arithmetic on two sourced inputs (Google's stated crawl window, Ahrefs' time-to-rank distribution) plus a placeholder you replace with your own data. Treat 91 days as a hypothesis to falsify with your own Search Console. My four-month ranking timeline, with real numbers, is in this 34-keyword case study.
FAQ
What is keyword seasonality?
The predictable rise and fall in search volume for a query across the year. Gutter cleaning peaks in autumn; tax software peaks before the filing deadline. It is a property of the query, not of your site, and it repeats closely enough year to year that you can plan against it.
How far in advance should I publish seasonal content?
Add your production time, Google's crawl window, and your own measured climb to a clickable position, then count back from the month demand starts rising — not the peak month. With the placeholder values in this post the page has to be live 77 days before onset, and briefed 14 days before that — 91 days in total. For an October peak that means publishing in mid-May and briefing at the start of it.
Can Google Trends show actual search volume?
No. Trends reports relative interest scaled 0-100 within the window and region you selected, normalised against total searches in that window. For absolute-ish monthly numbers you need Google Ads Keyword Planner, which reports rounded ranges rather than exact counts.
How much seasonal variation is worth building a page for?
My rule of thumb is a peak-to-trough ratio above roughly 1.5x, and I want a smooth curve rather than a jagged one, since Google warns that low-volume queries carry visible statistical noise. Below that, write the evergreen page and give it a seasonal section.
Should I update the same page every year or publish a new one?
Update the same URL. It keeps whatever authority it accumulated last season. Make the substantive edits in the dead season, not during onset.
Planning a year of content around demand instead of vibes? I build the forecast, the calendar and the pages that sit on it. See my services or get in touch.
Get the AI-SEO Content Checklist
A practical checklist for getting your own content cited by Google AI Overviews, ChatGPT, and Perplexity — not just ranked.
Browse all free guides →Run this on your own numbers
Content ROI & Payback Calculator — free, no signup, runs in your browser.
Want to implement this with guidance?
Santosh helps founders turn insights like this into real systems.
External Resources
Further Reading & Tools
Google Search Central
Official Google documentation on indexing, ranking, and Core Web Vitals
Ahrefs Blog
In-depth SEO research, keyword strategy, and link-building studies
Moz Learn SEO
Comprehensive SEO learning hub covering technical and on-page fundamentals
Search Engine Journal
SEO news, algorithm updates, and strategy guides