Back to the map
Language ModelsFoundation10h

Tokenization

How text becomes numbers, and why it costs what it costs.

A model does not read letters, and it does not quite read words either. Before any text reaches it, the text is cut into tokens: short pieces of characters drawn from a fixed vocabulary the model was trained on. A common word like "the" is one token; "unhappiness" might arrive as three; a space or a bracket is often a token of its own.

Everything a model charges you for and everything it limits is counted in tokens, and the ratio of tokens to words is not stable. Code, JSON, and text that is not English break into far more tokens than ordinary English prose, so a budget or a length check worked out in words or characters can be wrong by several times over.

It is the bottom of the stack for language models, and three things sit directly on it: context windows, which are measured in tokens; prompt caching, which matches on token boundaries; and embeddings, which tokenise text before turning it into numbers. Learn it first and the arithmetic in all of them stops being mysterious.

A token is a chunk of characters, not a word, and every limit and every bill is counted in tokens.

In more detailModels see tokens from a learned subword vocabulary, not characters. Token counts drive both price and context limits, and they are not proportional to character counts: code, non-English text and unusual formatting all tokenize far less efficiently than English prose. Different model families use different tokenizers, so a prompt that fits one may not fit another.

Section 1 of 4, 2h

What a token is

By the end: You can explain what the model actually reads, and predict roughly how many tokens a piece of text will be.

  • Why models do not read characters or words
  • Subword tokens, and what they look like
  • The rough rules of thumb for English
  • Why code and other languages cost more

Everything you will ever be charged for, and every limit you will ever run into, is counted in tokens. This section explains what one actually is, why the pieces fall where they do, and how to estimate the count of a piece of text before you send it. Code and languages other than English cost noticeably more for the same amount of meaning, which is the sort of thing worth knowing before it turns up on a bill.

Models do not read text#

How text becomes tokens
1 / 5
The quick brown foxFour words to you. Not necessarily four tokens.

Models do not read characters or words. They read tokens.A token is a piece of text the tokenizer learned to treat as one unit.

A language model cannot work with letters. Before anything happens, your text is converted into numbers. Each number stands for a piece of text called a token, and the model only ever sees that list of numbers.

Text:    "Tokenization is fun"

Tokens:  ["Token", "ization", " is", " fun"]

Numbers: [30642, 2065, 374, 2523]
Three words became four tokens. The model sees only the last line.

Notice two things. A long word split into two pieces, and the space before a word is part of the token rather than separate. Both are normal.

Why split words at all#

There are two obvious approaches and both fail. One token per character means even a short sentence becomes a very long sequence, and sequences are expensive. One token per word means the vocabulary must contain every word that exists, including every name, typo and new word, which is impossible.

Subword tokens sit in between. Common words get their own token. Rare words are built from pieces. A word the model has never seen can still be represented, because it can always fall back to smaller fragments.

"the"            -> 1 token    very common, gets its own
"tokenization"   -> 2 tokens   less common, built from pieces
"Kowalczyk"      -> 3 tokens   a rare name, built from fragments
"aaaaaaaaaaaa"   -> 4 tokens   nonsense, still representable

Rules of thumb#

For ordinary English prose, one token is roughly four characters, or about three quarters of a word. So 1,000 tokens is about 750 words, and a page of text is around 500 tokens.

These are estimates, not guarantees

Never build billing or a context-limit check on the four-character rule. Count properly with the tokenizer when the number matters. The estimate is for planning, not for enforcement.

Not all text costs the same#

Tokenizers are trained mostly on English, so English is the most efficient thing you can send. Everything else is worse, sometimes dramatically.

  • English prose: the baseline, roughly 4 characters per token.
  • Code: worse. Indentation, brackets and symbols each tend to become their own token.
  • JSON: much worse. Every brace, quote and colon is a token, so structure costs real money.
  • Languages not written in Latin script: often two to three times more tokens for the same meaning.
  • Long numbers: usually split into several tokens rather than one.
"Hello, how are you?"                    ->  6 tokens
'{"greeting": "Hello", "ok": true}'      -> 13 tokens
The same greeting, wrapped in JSON, costs roughly twice as much.

Where people go wrong

Estimating cost by counting words. The estimate is fine for planning and wrong for anything involving money or a context limit, particularly once your payloads contain JSON or code.

Build this: Take the same paragraph in English and in another language, tokenize both, and compare the counts. Then wrap one in JSON and compare again.

Open this section on its own page
Section 2 of 4, 2h

Counting tokens and what they cost

By the end: You can measure exactly what a request costs before and after you send it.

  • Counting tokens in practice
  • Input versus output pricing
  • Reading usage data from a response
  • Working out cost per request and per user

You know what a token is; this is how to find out how many you are sending and what that comes to in money. Input and output are priced differently, often by a factor of five, so the same total can cost very different amounts depending on which direction it went in. The section covers measuring before you send, reading the usage figures afterwards, and turning both into a cost per request and per user, which is the number somebody will eventually ask you for.

Count, do not guess#

Every provider exposes the token count of a response, and most let you count text before sending it. Use the real number whenever it affects cost or fits in a limit.

response = client.models.generate_content(
    model="gemini-3.5-flash-lite",
    contents="Explain tokenization in one sentence.",
)

usage = response.usage_metadata
usage.prompt_token_count        # what you sent
usage.candidates_token_count    # what came back

Input and output are priced differently#

Output almost always costs several times more than input. This matters because it changes where you should look when a bill is too high. A long prompt is cheaper than a long answer.

INPUT_PER_MILLION = 1.00
OUTPUT_PER_MILLION = 5.00

def cost(usage) -> float:
    return (
        usage.prompt_token_count / 1_000_000 * INPUT_PER_MILLION
        + usage.candidates_token_count / 1_000_000 * OUTPUT_PER_MILLION
    )

print(f"${cost(usage):.6f}")
Put the rates in config, not in the code. They change, and the code should not have to.

Where the cost actually goes#

In a chat or an agent, the input grows every turn because you resend the whole history. By turn ten you are paying to re-read nine previous turns, most of which is the model's own earlier output. That, not the length of any single message, is what makes long conversations expensive.

Turn 1:   7 input tokens
Turn 2:  33 input tokens
Turn 3:  48 input tokens
Turn 10: several hundred, for the same short question
Measured from a real three-turn conversation. The question stayed short; the history did not.

Log cost from the first day

Record tokens and cost for every call from the very start of a project. Adding it later means you cannot answer the first question anyone asks, which is what the expensive path is.

Where people go wrong

Measuring cost per request instead of per completed task. A cheaper model that needs three attempts is not cheaper. The number that matters is what it costs to get the job done.

Build this: Add a cost meter to a script that runs a three-turn conversation. Print running total cost after each turn, and watch the input count climb while your questions stay the same length.

Open this section on its own page
Section 3 of 4, 2h

Context limits and what fits

By the end: You can work out whether something will fit, and decide what to do when it does not.

  • What the context window is
  • That input and output share it
  • What happens when you exceed it
  • The first things to try when something does not fit

There is a ceiling on how much a model can consider at once, and it is not only your question that counts against it. This section is about working out in advance whether a request will fit, and what to do first when it will not. Exceeding the ceiling is not a graceful degradation: the call fails outright, and it tends to fail on your largest and most important request rather than on a test.

The window#

A model can only consider a fixed number of tokens at once. That budget is the context window, and it covers everything: the system prompt, the conversation history, retrieved documents, tool definitions, tool results, and the answer the model is about to write.

Context window
|<------------------------------------------->|
[ system ][ history ][ documents ][ reply room ]
The reply needs room too. If you fill the window with input, there is nowhere for the answer to go.

Exceeding it does not give you a partial answer. The request is rejected. So anything that assembles a prompt from variable-length parts needs to check before sending.

Bigger is not the same as better#

Context windows have grown very large, which tempts people to stop thinking about what they send. Two reasons not to. Cost scales with everything you include, every single call. And recall is not uniform across a long window: material in the middle is retrieved less reliably than material near the beginning or the end.

The most common wrong answer in a design round

'The window is a million tokens, so just put the whole database in context.' It is expensive, it is slower, and the model attends to the middle less well. Curating what goes in beats dumping everything in.

When it does not fit#

  1. 1.Send less. Retrieve the relevant sections instead of the whole document. This is usually both cheaper and more accurate.
  2. 2.Summarise older conversation turns and keep the summary instead of the transcript.
  3. 3.Split the work into several calls and combine the results.
  4. 4.Only then reach for a model with a larger window.

The order matters. Reaching for a bigger window first solves the error and leaves you with a system that is expensive and gets worse as it grows.

Where people go wrong

Forgetting to leave room for the reply. If input fills the window, generation has nowhere to go and the call fails or truncates immediately.

Build this: Write a function that assembles a prompt from a system message, a history and a list of documents, and drops the least important documents until the whole thing fits inside a token budget you pass in.

Open this section on its own page
Section 4 of 4, 4h

Byte pair encoding from scratch

By the end: You can train BPE merges on a corpus, encode and decode with them, and explain why the tokenizer's behaviour on code, numbers and non-English text follows from how the merges were learned.

  • The training loop: count adjacent pairs, merge the most frequent, repeat
  • Why the merge order is the model and must be saved
  • Encoding by replaying the merges, and decoding by concatenation
  • Keeping the leading space inside the token
  • Why byte-level BPE never fails on unseen input
  • What this explains about token counts on code and other languages

Every explanation of tokenization in this topic has treated the tokenizer as given: text goes in, tokens come out, and the mapping looks arbitrary. It is not arbitrary, and the algorithm behind it fits on a page. Writing it is the fastest way to stop being surprised by token counts, and it is the stage the from-scratch GPT project explicitly defers.

The idea#

Start with a vocabulary of single characters, which can represent anything. Count every adjacent pair in the corpus, merge the most frequent pair into a single new symbol, and repeat until the vocabulary is the size you wanted. Frequent sequences — common words, common word endings, common indentation — become single tokens because they were merged early; rare ones stay as several.

The output of training is not a list of tokens. It is an ordered list of merges, and the order is load-bearing: encoding replays the merges in the order they were learned, so a saved tokenizer is that list plus the character inventory it started from.

Splitting before merging#

Real tokenizers do not let merges cross word boundaries, because a token spanning two words would be learned from one corpus and useless in another. Text is first cut into chunks by a regular expression, and merges happen within a chunk. The convention every modern tokenizer follows is to keep a word's leading space attached to it, which is why " the" and "the" are different tokens and why a token count changes when you strip whitespace.

import re
from collections import Counter


def words(text):
    # a word with its leading space, or a run of whitespace on its own
    return re.findall(r" ?[^\s]+|\s+", text)


def merge_pair(symbols, pair):
    a, b = pair
    out, i = [], 0
    while i < len(symbols):
        if i + 1 < len(symbols) and symbols[i] == a and symbols[i + 1] == b:
            out.append(a + b)        # the two symbols become one
            i += 2
        else:
            out.append(symbols[i])
            i += 1
    return tuple(out)
Production tokenizers use a longer pattern that also splits numbers into digit groups and separates punctuation. The principle is the same.

Training#

Count pairs weighted by how often the word containing them appears, rather than walking the raw text, which is the difference between a loop that finishes and one that does not. Ties are broken deterministically, because a tokenizer that trains differently on two runs of the same corpus is a tokenizer you cannot ship.

def train(text, n_merges):
    freqs = Counter(words(text))                 # word -> how many times it occurs
    split = {w: tuple(w) for w in freqs}         # every word starts as its characters
    merges = []

    for _ in range(n_merges):
        pairs = Counter()
        for w, f in freqs.items():
            s = split[w]
            for pair in zip(s, s[1:]):           # every adjacent pair in the word
                pairs[pair] += f                 # weighted by the word's frequency
        if not pairs:
            break

        # most frequent, ties broken on the pair itself so training is reproducible
        best = max(pairs, key=lambda p: (pairs[p], p))
        if pairs[best] < 2:
            break                                # nothing left worth merging

        merges.append(best)
        split = {w: merge_pair(s, best) for w, s in split.items()}

    return merges
corpus = (
    "the cat sat on the mat. the cat sat on the hat. "
    "a cat and a hat and a mat. the rat sat on the cat. "
    "the fat cat sat. the fat rat sat. the mat and the hat."
)

for i, (a, b) in enumerate(train(corpus, 20)):
    print(i, repr(a), "+", repr(b), "->", repr(a + b))

# 0 'a' + 't'    -> 'at'
# 1 't' + 'h'    -> 'th'
# 2 'th' + 'e'   -> 'the'
# 3 ' ' + 'the'  -> ' the'
# 4 'at' + '.'   -> 'at.'
# 5 ' ' + 'a'    -> ' a'
# ...
# 8 ' c' + 'at'  -> ' cat'
# 11 ' s' + 'at' -> ' sat'
Merge 2 builds "the" out of an earlier merge; merge 3 then attaches its space. Later merges are always built from earlier ones, which is why the order cannot be shuffled.

Encoding and decoding#

Encoding replays the merge list against a fresh word. Decoding is concatenation and nothing else, which is the property that makes the leading-space convention work: the spaces are inside the tokens, so joining them reproduces the input exactly with no rules about when to insert a space.

def encode(text, merges):
    out = []
    for w in words(text):
        s = tuple(w)
        for pair in merges:          # in the order learned - this is the whole model
            if len(s) == 1:
                break
            s = merge_pair(s, pair)
        out.extend(s)
    return out


def decode(tokens):
    return "".join(tokens)           # the spaces are already in the tokens


merges = train(corpus, 20)
tokens = encode("the fat cat sat on a mat.", merges)
print(tokens)
# ['the', ' ', 'fat', ' cat', ' sat', ' on', ' a', ' mat.']

assert decode(tokens) == "the fat cat sat on a mat."
Eight tokens for twenty-five characters. " fat" is two tokens because that pairing never earned a merge in this tiny corpus, while " cat" did.

That split of " fat" into a space and "fat" is the algorithm's whole character in one line. Nothing decided that "cat" deserves its space and "fat" does not; " cat" appeared more often in the training corpus and got its merge before the budget ran out. Every oddity in a real tokenizer has the same shape.

This encode is correct and slow

Replaying every merge against every word is O(merges x length). Real implementations give each merge a rank and repeatedly apply the lowest-ranked applicable pair within a word, which reaches the same answer while touching only the merges that are relevant. Write the slow version first, because it is obviously correct, and keep it as the reference the fast one is tested against.

Unseen text, and why bytes#

A word the tokenizer has never seen still encodes: the merges that apply are applied, and whatever is left stays as single characters. Nothing fails.

t = encode("the elephant sat on a submarine.", merges)
print(t)
# ['the', ' ', 'e', 'l', 'e', 'p', 'h', 'a', 'n', 't', ' sat', ' on', ' a',
#  ' s', 'u', 'b', 'm', 'a', 'r', 'i', 'n', 'e', '.']

assert decode(t) == "the elephant sat on a submarine."
Unseen words cost far more tokens. This is the same effect that makes rare technical terms and non-English text expensive.

That guarantee only holds if the starting inventory covers every possible input, and a character-level start does not: the corpus above has no vocabulary for an emoji or a Cyrillic letter. Real tokenizers start from the 256 possible bytes rather than from characters, so every conceivable input is representable and an unknown token never has to exist. The cost is that one character outside ASCII is several bytes, and so potentially several tokens, before any merges apply.

Turning tokens into ids#

The model needs integers. The vocabulary is the starting inventory followed by the merge results, in merge order, so ids are assigned once at training time and the mapping is part of the saved tokenizer.

vocab = sorted(set(corpus)) + [a + b for a, b in merges]
ids = {t: i for i, t in enumerate(vocab)}

print(len(vocab))                    # 34
print([ids[t] for t in tokens])      # [16, 0, 31, 22, 25, 27, 19, 33]
A real vocabulary is 256 byte values plus 50,000-odd merges plus a handful of special tokens.

What this explains#

  • Code costs more tokens per character than prose, because indentation, brackets and camel-case identifiers were a smaller share of the training corpus and earned fewer merges. Some tokenizers add explicit merges for runs of spaces, which is why the same code costs different amounts across models.
  • Non-English text costs several times more, because byte-level starts mean a character outside ASCII is two to four bytes before merging, and those sequences were rare enough to earn few merges.
  • Numbers tokenize unevenly, because "2023" may be one merged token while "2024" is two. This is a documented contributor to arithmetic errors, and it is why several tokenizers now force digits to split into fixed groups.
  • A trailing space changes the token count and sometimes the output, because it becomes part of the next word's token rather than a token of its own.
  • The same string tokenizes differently under different models, so a token count is only meaningful alongside the tokenizer that produced it.

Where people go wrong

Treating a space as a separate token and reconstructing text by joining tokens with spaces. It appears to work on simple English and then destroys punctuation, code indentation and any language that does not separate words with spaces. Keeping the leading space inside the token makes decoding a pure concatenation with no rules at all, which is why every production tokenizer does it.

Build this: Train BPE on a few megabytes of real text — mixed prose and source code — targeting a thousand merges. Print the first fifty merges and the last fifty, and account for the difference. Then encode a paragraph of English, the same paragraph translated into a language written in another script, and a page of your own code, and compare tokens per character across the three. Finally, assert that decoding round-trips exactly on all three, including on text containing an emoji.

Open this section on its own page

Where you use this

A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.

Build your own GPT
  • Stage 1, Turn text into numbers: What a token actually is, and why models do not work in characters or words.
An agent that uses tools
  • Stage 5, The transcript grows: Counting what the transcript costs per step, before the cost is a line on an invoice.

Now actually do it

Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.

1Tokenize everything you send

3h

Take the same paragraph in English and another language, then as JSON, then as code. Count tokens for each. Then take one of your own real prompts and find which part costs the most.

Done when: You can name the most expensive component of one of your own prompts, in tokens.

2Build a cost meter

3h

Wrap a model client so every call logs input tokens, output tokens, cached tokens and cost. Run a three-turn conversation and watch the input count climb while your questions stay the same length.

Done when: You have the three numbers from a real conversation and can explain the growth.

6 hours of practice

Words used here

3 terms this topic teaches, each with a one-line definition if one of them stopped you.

Token
The unit a model actually reads and writes: usually a common word, a word fragment, or a few bytes of text.
Tokenizer
The component converting text into a sequence of integers and back, using a fixed vocabulary learned before training. Every model family has its own, so counts do not transfer between them.
Vocabulary
The fixed set of tokens a model can read or emit, typically tens to hundreds of thousands of entries. Its size sets the width of the embedding table and of the final output layer.

Interview questions

14 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.

What a token is

What is a token?

A piece of text that the model treats as one unit, usually a subword. Text is converted into a list of token ids before the model sees it, and the model only ever works with those ids.

Why use subword tokens rather than words or characters?

Characters make sequences too long, and sequence length drives cost and compute. Whole words need an impossibly large vocabulary and cannot handle unseen words. Subwords give a fixed vocabulary that can still represent anything.

Roughly how many tokens is 1,000 words of English?

About 1,300, using the rule that a token is around three quarters of a word. For anything that matters, count with the tokenizer rather than estimating.

Why does the same content cost more in some languages?

Tokenizers are trained mainly on English text, so English gets efficient single tokens for common words. Other scripts fall back to smaller fragments, often two to three times as many tokens for the same meaning.

Counting tokens and what they cost

Why is output usually more expensive than input?

Generating tokens is sequential work: the model produces one token at a time, each conditioned on all the previous ones. Reading input is done in one parallel pass. The pricing reflects the difference in compute.

Your chatbot costs $0.40 per conversation and needs to be under $0.10. Where do you look first?

Input tokens across the whole conversation, because history is resent every turn and grows quadratically. Prompt caching on the stable prefix, trimming or summarising old turns, and routing simple turns to a cheaper model, in that order.

How would you estimate the monthly cost of a feature before building it?

Take a representative prompt and response, count both, apply the per-million rates, and multiply by expected requests. Add a margin for retries and for conversations being longer than the sample.

Context limits and what fits

What is a context window?

The maximum number of tokens a model can process in one request. It includes the system prompt, the full conversation history, any retrieved content, tool definitions and the response being generated.

What happens if you exceed it?

The request is rejected rather than truncated. Anything building a prompt from variable-length pieces has to check the size before sending.

If a model has a very large context window, why not put everything in it?

Cost scales with every token on every call, latency grows, and recall across a long window is uneven, with material in the middle attended to least reliably. Retrieving the relevant part is usually both cheaper and more accurate.

A user uploads a 500-page PDF and asks a question about it. How do you handle it?

Do not send all of it. Chunk the document, index it, retrieve the sections relevant to the question, and send only those with citations back to the source. That keeps cost flat as documents grow and lets you show where the answer came from.

Then they push further

The question is 'summarise the whole thing'. Does retrieval still work?

No, because the answer depends on all of it. Summarise in stages instead: summarise sections, then summarise the summaries, keeping track of which section each point came from.

What gets marked up. Strong answers separate questions that need one part of the document from ones that need all of it.

Byte pair encoding from scratch

Implement byte pair encoding: training, encoding and decoding.

Training: split the corpus into words, represent each as a tuple of bytes or characters, then repeatedly count every adjacent pair weighted by word frequency, merge the most frequent pair everywhere, and record it. The output is the ordered merge list. Encoding: split the input the same way and replay the merges in order against each word. Decoding: concatenate the token strings, which works exactly because leading spaces are kept inside the tokens.

Then they push further

Why does the order of the merge list matter?

Because later merges are built from the symbols earlier merges created. "the" only exists as a mergeable symbol after "t"+"h" and then "th"+"e" have happened, so " "+"the" cannot be applied before them. Replaying in a different order gives a different and generally worse segmentation, so the order is the model.

Why start from bytes rather than characters?

So that nothing is unrepresentable. There are 256 possible bytes and every input is a sequence of them, so an unknown token can never occur, whatever alphabet or emoji arrives. A character-level inventory is open-ended and would need an unknown token that the model cannot decode back.

Your encode loops over every merge for every word. How would you make it fast?

Give each merge a rank from its position in the list, then within a word repeatedly find the applicable pair with the lowest rank and apply it, stopping when no pair is applicable. That touches only relevant merges instead of all of them, and produces the same segmentation. Keep the slow version as the test oracle.

What gets marked up. Candidates who have written it keep the leading space inside the token and know decoding is plain concatenation. Handling a space as its own token, and then needing rules about when to re-insert it, is the sign of someone recalling the idea rather than having implemented it.

Why is a tokenizer trained rather than written?

Because which sequences deserve to be one token is a property of the corpus, not of the language. Frequent sequences get merged early and become single tokens; rare ones stay split. That is why the same tokenizer is efficient on the text it was trained on and expensive on code or on a language that was a small share of the training data, and why the merge list has to be shipped with the model.

Why do models struggle with arithmetic and with spelling questions?

Both follow from the segmentation. A number may be one token or three depending on whether that digit sequence was frequent enough to merge, so digits that should be treated positionally arrive in inconsistent groupings — several tokenizers now force digits into fixed groups for exactly this reason. Spelling questions fail for the same reason from the other direction: a word that arrives as a single token gives the model no direct view of its letters.

These questions on their own page, one link per question

Resources

Free to watch and read, picked for this topic.

Stop 3 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeThe Messages APIStateless request in, one message out.6h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.