Skip to content
beginner Phase 4 · Tokenization and Context Windows

How Tokenizers Work

See how byte-pair encoding splits text into subword tokens across languages and code.

40m
0 problems
Topic Progress 0%

The Count That Lied

A summarizer that passes on the English test set dies in its first production week. The trace shows 400 context_length_exceeded. The customer's message is 6,000 characters — shorter than the test messages that worked. Character count lied, because the model never sees characters: a tokenizer maps text to integer IDs first, and both the context window and the invoice are measured in those IDs.

Run a counter and the mismatch is immediate:

import tiktoken

enc = tiktoken.get_encoding('o200k_base')   # ships with the model, not with your code

samples = {
    'english': 'The quickest way to double your money is to fold it in half and wait.',
    'chinese': '把你的钱对折再等等,这是最快的翻倍方法。',
    'emoji': 'ship it 🚀🔥🎉',
}
for name, text in samples.items():
    print(name, len(text), '->', len(enc.encode(text)))

Input: the three strings above. Output:

english 69 -> 16
chinese 20 -> 14
emoji 11 -> 7

len(text) counts characters; len(enc.encode(text)) counts tokens — the vocabulary IDs the model will actually consume. The full picture, including the popular shortcut:

Text Characters Tokens chars / 4 Estimate error
English sentence 69 16 17 about right
Chinese sentence 20 14 5 2.8x too low
ship it 🚀🔥🎉 11 7 2.8 2.5x too low

Under the hood the encoder is a table of roughly 200,000 vocabulary entries mapped to integers, and enc.decode(ids) reverses the mapping exactly for the text you encoded — the model's entire world is those integers. Punctuation joins the count too: wait. costs 2 tokens, so one-token-per-word is a second myth stacked on top of characters.\n\nThree consequences fall out of those rows. Non-English scripts and emoji consume more tokens per character than plain ASCII, so a string-length guard that protects English traffic still lets oversized CJK traffic through. Word boundaries are not token boundaries, so word counts do not translate either. And the ratio shifts with the content, so no constant repairs the estimate — you can only measure.

The measurement feeds two systems you build next: Context Window Budgeting counts these IDs against the model's limit, and Estimating Token Costs converts them into money. Both assume you know how the split is produced, which is the question byte-pair encoding answers.

A Byte-Pair Encoding Walkthrough

A fixed vocabulary cannot contain every word, and splitting every word into raw bytes would make every sentence expensive. Byte-pair encoding finds the middle: start from characters, count adjacent pairs, merge the most frequent pair, repeat. The resulting merge list is the tokenizer — learned from frequency statistics during training and frozen afterward.

from collections import Counter

corpus = ['low'] * 5 + ['lower'] * 2 + ['newest'] * 6 + ['widest'] * 3
words = [list(w) + ['</w>'] for w in corpus]   # </w> marks the word boundary

def apply_merge(word, pair):
    out, i = [], 0
    while i < len(word):
        if i + 1 < len(word) and (word[i], word[i + 1]) == pair:
            out.append(word[i] + word[i + 1])   # the pair collapses into one token
            i += 2
        else:
            out.append(word[i])
            i += 1
    return out

merges = []
for _ in range(4):
    pairs = Counter()
    for w in words:
        pairs.update(zip(w, w[1:]))
    best = pairs.most_common(1)[0]              # most frequent adjacent pair wins
    merges.append(best)
    words = [apply_merge(w, best[0]) for w in words]
    print('merge', best)

Input: the 16-word corpus above. Output:

merge (('e', 's'), 9)
merge (('es', 't'), 9)
merge (('est', '</w>'), 9)
merge (('l', 'o'), 7)

Read the merges as a story. est occurs six times in newest and three times in widest, so it wins the first round; the end-of-word marker glues on next, and est</w> becomes one token. Encoding a new word replays those merges in training order:

def encode(word, merges):
    tokens = list(word) + ['</w>']
    for pair, _ in merges:
        tokens = apply_merge(tokens, pair)      # fixed order, not frequency order
    return tokens

print(encode('newest', merges))   # ['n', 'e', 'w', 'est</w>']
print(encode('lowest', merges))   # ['lo', 'w', 'est</w>']
print(encode('qzxv', merges))     # ['q', 'z', 'x', 'v', '</w>']

lowest never appeared in training, yet it costs three tokens because its suffix was learned from other words. qzxv matches no merge and stays at one token per character. The pattern generalizes: common English words map to one token because they appeared whole often enough to earn vocabulary slots (hello -> 1), while rare and unseen strings fall back to frequent pieces (antidisestablishmentarianism -> 6, asdfghjkl -> 4). Token count tracks familiarity with the training corpus, not string length — which is exactly why the character math in the previous chapter fails.

The loop is the whole mechanism: characters become frequent pairs, pairs become merged units, and each merge appends one line to the tokenizer's table. Two models with different merge lists count the same sentence differently, so the counter you run must match the model you call.

Counting Before You Send

Character ratios and merge lists explain the split; production needs a number before the request leaves your process. Count locally with the encoding the target model uses:

import tiktoken

def fits(text, limit, encoding='o200k_base'):
    n = len(tiktoken.get_encoding(encoding).encode(text))
    if n > limit:
        raise ValueError(f'prompt is {n} tokens, limit is {limit}')  # fail here, not as a 400
    return n

Input: fits('assembled prompt text', 8000). Output: the token count, or a ValueError naming both numbers — raised while the user still sees a spinner rather than an error page.

The counter itself is cheap: tiktoken keeps the merge table locally, so the check runs inside your request path — about a millisecond for a 3,000-character prompt — and no prompt text leaves your process just to be counted.

The pre-check must cover everything that rides along: system prompt, conversation history, retrieved chunks, tool and schema definitions, and image parts. A check over the user's message alone approves requests the API later rejects.

Four counting failures seen in real services:

  • chars / 4 in a guard. Passes English staging traffic, breaks on the first CJK or emoji-heavy message, as the earlier counts predicted.
  • The wrong encoding. Vocabularies differ between model families and versions; counting with the previous model's tokenizer can drift by tens of percent. Pin the encoding to the model you call.
  • Counting only the user's turn. System prompt, history, retrieval, tool schemas, and images all add tokens; every uncounted part shrinks the real margin.
  • No local check at all. Each oversized request costs a full round trip, a 400, and often a blind retry that repeats the identical failure.

Counts also move when you edit the prompt: 200 tokens added to the system prompt subtract 200 from the history budget in Context Window Budgeting and multiply the bill in Estimating Token Costs. The ID sequence streaming back to the user in Streaming Responses is this same vocabulary — the count you make here is the count that matters there.

Quiz

1. A 600-character Chinese message is rejected while a 900-character English message passes. Why can character counts not decide which fits?

Question 1 options

2. During byte-pair encoding training, what does the algorithm repeatedly merge?

Question 2 options

3. Why does `antidisestablishmentarianism` cost 6 tokens while `hello` costs 1?

Question 3 options

4. Before dispatching a request, what is the only reliable way to know whether the prompt fits the window?

Question 4 options

Flashcards

Question

What does a tokenizer actually produce for the model?

Answer

A sequence of integer IDs from a fixed vocabulary. Window limits, billing, and the model's input are all measured in those IDs, never in characters or words.

Question

How does byte-pair encoding build its vocabulary?

Answer

Start from characters (plus an end-of-word marker), repeatedly merge the most frequent adjacent pair in the training corpus, and freeze the merge list. Encoding replays those merges in the fixed training order.

Question

Why do rare words cost more tokens than common words?

Answer

Common words appear whole often enough to earn a single vocabulary slot; rare or unseen strings fall back to frequent subword pieces. Token count tracks training-corpus familiarity, not string length.

Question

Why does the characters-divided-by-4 heuristic fail?

Answer

It roughly matches English prose but underestimates CJK text and emoji by 2-3x, because tokens per character varies by language and content. Only encoding with the model's real tokenizer is reliable.

Question

Where should token counting happen in an LLM feature?

Answer

Locally, before dispatch: encode system prompt, history, retrieval, and tool schemas, then reject anything over budget. Oversized requests should fail in your code, not as a provider 400 after a full round trip.

Revision Notes

Key Takeaways

  • 1. The model consumes integer token IDs; windows and invoices are denominated in tokens, not characters or words
  • 2. BPE learns merges from pair frequency, which is why common words stay whole and rare words fragment
  • 3. CJK and emoji text cost 2-3x more tokens per character than the English heuristic predicts
  • 4. Pre-encode the full assembled prompt locally and fail fast before dispatching

Interview Tips

  • • Walk the interviewer through a BPE merge step by step on a tiny corpus — pair counts, the winner, and the resulting vocabulary growth
  • • Be ready with concrete numbers: one English sentence of 69 characters encoding to 16 tokens beats any hand-waving about ratios
  • • Explain the business consequence of miscounting: an uncaught oversized prompt is a user-visible error, a wasted round trip, and unpriced spend
  • • Connect counting to the next design decision — how many history turns or retrieval chunks fit — which is where [Context Window Budgeting](/topic/aie-context-window-budgeting) takes over

Cheat Sheet

Tokenizers Cheat Sheet

BPE in one line: count adjacent pairs -> merge the most frequent -> repeat; the merge list is the tokenizer, learned from training statistics and frozen afterward.

Counting tokens:

import tiktoken
enc = tiktoken.get_encoding('o200k_base')
n = len(enc.encode(text))

Measured ratios (o200k_base):

Text Characters Tokens
English sentence 69 16
Chinese sentence 20 14
ship it 🚀🔥🎉 11 7

Facts to keep:

  • Common words map to one token (hello -> 1); rare words split (antidisestablishmentarianism -> 6).
  • Non-English scripts and emoji burn more tokens per character than ASCII.
  • chars / 4 works for English only; never use it as a production guard.
  • Count everything that rides along: system prompt, history, retrieval, tool schemas, images.
  • Pin the encoding to the model you call; vocabularies differ across families and versions.

Failure pattern: guard written in chars/4 passes staging (English), then CJK traffic triggers 400 context_length_exceeded in production.

Continue Learning

Before You Start

Related Reading