
I opened thirty token counters this week looking for a file input. Thirty pages, every one a bare text box. Not one had type="file" anywhere in the markup.
The question behind most of these searches is whether a document fits in one request. The tool that answers it will not take a document. You open the PDF, select all, copy, paste, and hope the clipboard held it. Then you find out the page has no idea which model you meant.
The token counter takes the file. It also says which of its numbers are real and which are guesses, and that second part turned out to be the harder half.
I ranked this thirtieth out of 1,761. Score 9.3. About twelve hours on the card.
The Question Is Never "How Many Tokens"
Nobody wakes up wanting a token count. They want to know if the thing fits, or what it will cost, or which part of the prompt got fat.
A support agent has a system prompt that grew for six months and now the model forgets the first instruction. A contractor has a 60 page spec and a 200K window. Someone has an API bill that doubled and no idea which call did it.
The count is never the answer. It is the number you need to get to the answer.
That is why the context window bar sits next to the count on this page, and why the file goes in whole.
Thirty Pages, One Text Box
I checked the field before building anything. Thirty pages that rank for token counter, opened and read.
None of them has a file input. Zero occurrences of type="file" across all thirty. The counters that are genuinely good, tiktokenizer, netraruntime, gptforwork, are all still a text box.
Two of them are gone entirely. token-counter.app now redirects to a pricing comparison site. quizgecko.com/tools/token-counter returns a 301 to its homepage and its footer no longer lists the tool at all. tokencounter.net answered 502 the day I checked. tokencounter.io, which half the blog posts about this cite, does not resolve.
The field has holes in it because the work is dull and it never stops. Model names change every few months, and a stale model list is worse than no list at all.
The Number Depends on the Model, Which Nobody Tells You
A token is not a word and not a character. It is a chunk of bytes one model learned to treat as a unit, and every family learned a different set of chunks.
The word antidisestablishmentarianism is six tokens on GPT-5. A waving hand emoji is two tokens on GPT-5 and three on GPT-4 Turbo, because the newer encoding has a single token for it and the older one has to spell it out in bytes. Russian costs about twice what the same paragraph costs in English.
| Text | o200k_base, GPT-4o and later | cl100k_base, GPT-4 and 3.5 |
|---|---|---|
hello world | 2 | 2 |
| A waving hand emoji | 2 | 3 |
Привет, мир! Как дела? | 8 | 12 |
こんにちは、世界! | 4 | 6 |
Those are not estimates. They come from the test plan that ships with the tokenizer, which is OpenAI's own expected output. The page checks itself against those fixtures every time the tests run.
Now look at what most counters do with that. tokencounter.space uses cl100k_base for everything and calls it "compatible with most modern large language models". pricepertoken counts 300 models with cl100k and puts the result in a pricing table that looks exact. cl100k is not even OpenAI's current encoding.
The Three Kinds of Number
Once you accept that the model matters, you have to admit that you cannot count all of them. That is the part that took me the longest to get right.
OpenAI publishes its tokenizers. Meta publishes the Llama 3 one. Those run in the browser and the answer is the same number the API bills. Call that exact.
Anthropic has published nothing since Claude 2. The @anthropic-ai/tokenizer package on npm is still there, last published in July 2023, and its own README carries a warning: as of the Claude 3 models the algorithm is no longer accurate. Google publishes a tokenizer for Gemma, not for Gemini. Both vendors will count for you over an API with your key.
So for Claude and Gemini the page shows a range, and it labels the range an estimate. The range is not invented. Anthropic publishes that 1M tokens is roughly 555k words or 2.5M characters. Google publishes that a token is about four characters and 100 tokens is 60 to 80 English words. Two published ratios, two answers, and the honest output is the bracket between them.
The third kind is the one nobody else has: assumed. tiktoken 0.14.0 has no mapping for the gpt-6 names. Ask it and it raises a KeyError. The page still counts GPT-6 Astra with o200k_base, because that is the sensible guess, and says on the badge that it is a guess.
Claude Changed Tokenizer and Everyone's Old Number Is Wrong
Here is a detail buried in Anthropic's docs that breaks every counter written before this summer.
Claude 4.7 and later use a newer tokenizer. Same text, about thirty percent more tokens. Anthropic says so outright and tells you to recount rather than reuse a number measured against an earlier model.
That means one Claude ratio is wrong for half the lineup. This page carries two: 2.5 characters a token for Fable 5.1, Opus 5 and Sonnet 5, the older 4 characters a token for Haiku 4.5, which predates the change. It is a small thing that took ten minutes to implement and it is the difference between a bracket that holds and one that quietly under-reports by a third.
The Number You Are Billed Is Not the Number on the Page
This one is worse, and it is the reason two honest tools can disagree by 80 percent.
Anthropic's count_tokens endpoint, driven through gptforwork's tokenizer page, returns 9 for hello world. Count the same two words with a tokenizer and you get 2. The gap is the message envelope: role markers, separators, the structural tokens every chat request carries. Roughly seven tokens of it, before your text.
Neither number is a lie. One counts your text. The other counts a request. Every counter in the field, including this one, counts the text.
OpenAI at least documents its envelope. Its cookbook gives 3 tokens for each message wrapper and 3 more to prime the reply, and the page adds those up for you when the model is an OpenAI one. For everyone else there is no published figure, so the page says so and stops rather than making one up.
The Split Is Where the Money Is
The tiles under the box are real token boundaries, not a decoration. Long words break into pieces. A space rides along with the word after it. A tile ringed in amber is one character that cost several tokens.
Then there is the line table. It ranks the lines by what they spend, and it is the piece I use most.
I pasted a system prompt with a small base64 image in it. Three lines of instruction, one line of base64. The base64 line was 56 tokens, 82 percent of the whole thing. That is a number you cannot see by reading the prompt, and it is the entire answer to "why is this call so expensive".
Pasted logs do the same thing. Markdown tables do the same thing. The instructions you agonised over are usually the cheap part.
Reading a File Is Not Hard
The site already had a document parser. The ATS scanner uses it to read a resume: pdf.js for PDF, mammoth for DOCX, a FileReader for everything else. All of it in the browser.
Wiring it into the counter was mostly widening the list of formats it would accept. It read PDF, DOCX, TXT and Markdown. A prompt is as often a JSON payload, a YAML config or a Python file, and all three were being refused as unsupported. Now they are not.
One honest note that belongs on the page and is on it: a PDF pulls the pdf.js worker from a public CDN the first time. The file itself never leaves the tab, but a request does go out, and a tool that says "nothing leaves your browser" while quietly fetching a worker is doing the same thing I complain about in everyone else.
The Bug That Only Shows Up on Emoji
A build note, because it cost me an hour and it is the kind of thing that silently corrupts a visualisation.
To draw the split you need the bytes each token stands for. gpt-tokenizer exposes decode, so decoding one token at a time seems obvious. It is wrong. Its decoder keeps state between calls, so decoding a token that ends mid character leaves the leftover bytes in a buffer and the next call gets them.
Call decode([28823]) on its own and you get an empty string. Then call decode([233]) and you get a waving hand, which is not that token's text at all: it is the tail of the previous one plus this one. On plain English nothing looks wrong. On emoji, on Chinese, on anything multi byte, every tile shifts.
The fix was to read the byte table directly and rebuild UTF-8 myself, one group at a time, so a character split across four tokens comes back as one tile marked "4 tokens". There is a test that fails if the tokenizer package ever changes the shape of that table, because the whole visualisation rests on it.
What It Loads and When
The vocabularies are big. o200k_base is 2.3 MB of byte pair ranks. cl100k_base is 1.1 MB. The Llama 3 one is 3.2 MB.
None of it downloads when you open the page. The first keystroke starts it, or four seconds of you sitting there reading, whichever comes first. Someone who lands from a search and reads the copy pays nothing for a vocabulary they never used.
After that it is in the browser cache and the counting is instant. You can disconnect and keep counting, which is the actual test of a browser only tool.
What This Page Will Not Claim
It will not tell you the API bill to the token, because it counts your text and not your request.
It will not call Claude or Gemini exact, because nobody outside those companies can.
It will not run a heuristic and present it as a count. Three of the pages I opened do exactly that. One says "calculate the exact token count" in its headline and "*this tool estimates tokens assuming 1 token ~= 4 characters" in the footnote directly below it. Another overcounts by 13 percent against three tools that agreed with each other.
It will not remember your text on a server, because there is no server in the loop.
Where It Sits Next to the Other Tools
If you are counting because something is too long for a platform rather than a model, the word and character counter has the limits for tweets, bios, meta descriptions and SMS segments.
If the expensive line in your prompt turns out to be a JSON blob, the JSON formatter will minify it before you paste it back.
And if you are counting a resume because you are about to hand it to a model, Jobbi: AI Resume does the rewriting and the applying end to end.
The Short Version
Pick the model first, because the number is meaningless without it. Read the badge, because exact and estimate are different claims. Drop the file rather than pasting it, because the clipboard is not a document reader. Watch the line table, because the expensive part of a prompt is almost never the part you wrote by hand.
The token counter is free, needs no account, and the text stays in your tab.
Last updated: September 9, 2026 | Reading time: 10 minutes
Written by Evgeniy Poznyak, who spent an hour debugging a tokenizer that was politely handing him the previous token's leftovers.