DEV Community

Cover image for Every paid API tutorial ships a rate limit you can beat in four seconds
George Kioko
George Kioko

Posted on

Every paid API tutorial ships a rate limit you can beat in four seconds

There is a whole genre of tutorial that takes you from a database table to a paid public API in about twenty minutes. Table of data, a keys table, hash the key, put an edge function in front, add a rate limit, ship it. The good ones are genuinely good. The key handling in particular is usually correct, and correct here matters more than it sounds.

But almost every one of them ends with a rate limit you can walk around in four seconds, and most of them do not tell you.

The part they get right

Store a fingerprint of the key, never the key. You hash it, you keep the hash, you keep a short prefix so a customer can tell their own keys apart in a dashboard, and you hand the real string to the customer exactly once. If your database leaks, nobody gets a working key out of it.

That is not paranoia, that is the same shape Stripe and everyone else uses, and it costs you one hash function. Generate the key inside the database rather than in a browser so the raw value never travels further than it has to. Put row level security on the keys table so a user can only ever see their own rows. Keep the service role key inside the edge function, never anywhere a browser can reach it.

None of that is advanced. All of it is worth doing on day one, because retrofitting key hygiene after you have customers is miserable.

The part that quietly does not work

Then comes the rate limit. Count the requests for this key in the last minute, and if the count is over your limit, return a 429.

Read that again. For this key.

Your customer is sitting in a dashboard that lets them mint new keys whenever they like. They hit the limit, they click generate, they get a fresh key with a fresh counter, and they carry on. The limit measured the key. The person was never limited at all.

I am not describing a hypothetical. This exact bypass gets demonstrated live in tutorials, hit the ceiling, mint a new key, keep pulling data, and the narration moves on. Sometimes the author flags it as something to solve later. Later never arrives, because the reader has already copied the code.

Count the account, not the credential

The fix is a one word change in your head and a small change in your schema. A key is not a customer. A key is one of several credentials that belong to a customer. So the counter belongs on the customer.

Your keys table already stores the user id that owns each key. When a request arrives, you hash the key, look it up, and you now know two things, that the key is valid and who it belongs to. Do the counting on the second one. Every key that account owns draws from the same bucket.

Once you do that, minting a new key gets you nothing, which is exactly what you want, because minting a new key was never supposed to be a purchase.

While you are in there, cap how many keys an account can hold at all. Nobody needs four hundred. Rotation needs two. And keep the counter in a table the customer cannot write to. A limit stored somewhere the customer can edit is not a limit, it is a suggestion.

The bigger thing hiding underneath

Here is what took me too long to understand while building paid data tools. A rate limit is not billing. It is a blast shield. It stops one bad actor taking your service down, it does not decide what anyone owes you.

Metering is what you actually sell, and metering has a rule that rate limiting does not. You must never charge for a request that produced nothing useful.

That sounds obvious and almost nobody does it. Every broken data product i have ever taken apart, including two of my own, had the same defect. Loose match, return something anyway, charge for it anyway. Asked for one company, got a similar looking one, billed as a hit. Asked for a profile, got a page that merely mentioned the name, billed as a hit.

The honest version is not harder to build. Emit everything you found, mark what did not clear the bar, and charge only above the bar. Your dataset gets a row saying this one is free and here is why. Customers notice this immediately and it buys more trust than any amount of uptime.

Most of the time the confidence score you need is already sitting in your code, computed and then ignored. Go and look. i have found it every single time.

Three more things the twenty minute version skips

Version the path from the first request you ever serve. Once someone else's code depends on your response shape, changing that shape is not an improvement, it is an outage you caused. Ship v1 in the url and you can build v2 next to it without breaking anybody.

Paginate by default, and let people filter and sort on your side. If your answer to a thousand rows is here are a thousand rows, every customer builds the same filtering logic separately and all of them think your API is slow.

Return honest status codes. Bad key is a 401. Missing thing is a 404. Over the limit is a 429. Never a 200 with an error hidden in the body, because the caller's code reads the number before it reads anything else, and a lie there turns into a support ticket a week later when their pipeline has been silently storing failures.

And the thing nobody wants to hear

You can do all of this perfectly and still make no money, because none of it is the hard part.

The hard part is that a stranger has to find your API, believe it works, and hand over a card. i maintain seventy six of these things. The ones that earn are not the ones with the best code. They are the ones where somebody types a phrase into a marketplace search box and my title is the thing that comes back.

Build the key system properly, it takes an afternoon. Count against the account, not the key. Charge only for what worked. Then spend the rest of your time on the part that is actually hard, which is being findable.

Top comments (0)