Why my plan limits count queries, not keywords
My free plan says five keywords. That's the number on the pricing page, because it's the number a person can hold in their head.
It is not the number the code enforces. The limit that actually matters is a monthly count of queries, and the gap between those two ideas is where I lost an afternoon and learned something I'd have preferred to know first.
Cap the keywords and you've capped the wrong thing
Every lookup costs me money. A keyword sitting in the database costs nothing at all — it's a row. So "five keywords" reads like a cost limit while measuring something free.
Which leaves a loophole anyone would find by accident:
- Add five keywords. Each one triggers a lookup, because you want your first numbers immediately rather than tomorrow.
- Delete them.
- Add five more. Five more lookups.
You are permanently within your five-keyword limit and you have spent as many credits as you like. Nobody has to be malicious for this to happen — someone experimenting with which terms to track does it in the first ten minutes, honestly and with no idea it costs anything.
The fix isn't a stricter keyword cap. It's counting the thing that costs money:
# Monthly SERP-query ceiling for free users, counted from the usage log.
# add/delete churn and manual rechecks share the same budget
# (limit the query, not the keyword count — that's what closes
# the add-delete loophole).
free_monthly_queries: int = _int("FREE_MONTHLY_QUERIES", 150)
The keyword and site caps still exist, but their job changed. They're no longer the cost control — they're resource guards, sized so that a plan used to its limits still fits inside its query budget. Free is 5 keywords across 3 sites; Solo is 25 across 10. The point is that no combination of them produces a bill I didn't plan for.
Then I did the arithmetic on my own free tier
Free plan: five keywords, 150 queries a month, checked once a day by a cron. Search depth ten, which — as I've worked through elsewhere — caps a keyword at one credit per check.
5 keywords × 30 days × 1 credit = 150 credits
Monthly quota = 150
Exactly. Not approximately — exactly.
Which is fine as sizing. It's what you'd design on purpose: the automatic scan consumes the allowance, and the allowance is set so the scan is affordable.
What I hadn't noticed is what it does to quota usage as a signal.
The 80% email that would have gone to everyone, every month
I was about to build something obvious: when a free user reaches 80% of their monthly quota, email them an upgrade offer. They're clearly getting value. They're near a limit. Perfect moment.
Except run the numbers on who receives that email.
A free user with five keywords hits 100% of their quota every single month, because the cron spends it for them whether they visit or not. They cross 80% around the 24th, forever. Someone who set up five keywords once in March and never logged in again crosses 80% in April, May, June and every month after that.
Quota usage isn't a measure of engagement. It's a measure of how many keyword slots are filled, multiplied by the number of days in the month. It's arithmetic wearing the costume of a behavioural signal.
Had I shipped it, the "you're approaching your limit — here's 25% off" email would have gone to every active free account, monthly, forever. Which isn't a targeted offer. It's a permanent price cut with a mailing list attached, and after two or three months it's a spam complaint.
The tell, in hindsight: if a metric can be predicted from a user's configuration and a calendar, without knowing anything they did, it isn't measuring them.
What a real signal looks like
The honest version of "this person has outgrown the free plan" is a wall they walked into on purpose. There are only a few, and the product already knew about all of them — it just wasn't writing them down:
- They tried to add a sixth keyword and the cap said no.
- They tried to add a fourth site.
- They clicked a re-check button that's paid-only.
Each of those is a person asking for more and being refused. That's an event with a timestamp and an intent behind it, and it happens once — not on the 24th of every month until the end of time.
There's a softer version too, which I'll allow: every keyword slot full. It doesn't prove someone wanted more, but it does mean they set the thing up properly, and it still only fires once per account rather than monthly.
Both of those needed one line of logging each at the point where the limit already said no. That's the whole implementation. The expensive part was noticing that the metric I'd have reached for first was structurally meaningless.
The bit worth stealing
Two rules, both cheap, both learned the slow way:
A limit should measure whatever costs you money. Not the thing that's easiest to explain, and not the thing that appears in your data model. If your bill scales with API calls, count API calls — then work out which human-readable number to print on the pricing page, and make sure it can't be exceeded without tripping the real one.
Do the arithmetic on your own free tier before you build on top of it. Not "can I afford it" — I'd checked that. I mean: multiply the limits together and see what the numbers mean. Mine multiplied out to a metric that looked like enthusiasm and was actually a calendar. Ten minutes of arithmetic would have saved a feature that was designed to annoy every customer I have.
Written while building RankJot — Google rank tracking that emails you when your positions move. There's a free checker with no signup if you just want a one-off look.