The short answer: one English word averages 1.3 to 1.5 tokens across modern tokenizers, but the real number depends entirely on which tokenizer you’re using and what text you’re feeding it. GPT-4’s cl100k_base tokenizer yields roughly 1.3 tokens per word on typical English prose, while Llama 3’s tokenizer runs closer to 1.5. Code, non-English languages, and formatting characters push these ratios significantly higher.
What a token actually is
A token is an integer ID from a fixed vocabulary that a language model reads and writes. Tokenizers map raw bytes or Unicode code points to these IDs using a learned merge table (BPE) or similar algorithm (Unigram, WordPiece). The vocabulary size typically ranges from 32k to 256k entries. Each entry represents a byte sequence — sometimes a whole word, sometimes a subword fragment, sometimes a single character.
# GPT-4 (cl100k_base) vocabulary size: 100,277
# Llama 3 vocabulary size: 128,256
# Typical BPE vocabularies: 32k–256k
The tokenizer runs before the model sees anything. It’s a deterministic, reversible (mostly) preprocessing step. The model never sees “hello” — it sees token ID 15339. It never sees “ing” — it sees token ID 286. This mapping is fixed at training time and baked into the model weights.
How tokenizers split words
Byte Pair Encoding (BPE) builds its vocabulary by iteratively merging the most frequent adjacent byte pairs in a training corpus. Starting from 256 byte values, it learns merge rules like:
t + h → th
th + e → the
the + r → ther
ther + e → there
At inference time, the tokenizer applies these merges greedily, longest-match-first. “therefore” might become there + fore (2 tokens) or the + re + fore (3 tokens) depending on what merges exist in the vocabulary.
Unigram (used by T5, mT5, Llama) works differently: it starts with a large candidate set and prunes using a probabilistic language model, keeping pieces that maximize likelihood of the training data. The practical result is similar — subword pieces — but the segmentation boundaries differ.
Concrete ratios by tokenizer and language
| Tokenizer | Model family | Vocab size | English tokens/word | Spanish tokens/word | Python code tokens/word |
|---|---|---|---|---|---|
| cl100k_base | GPT-4, GPT-3.5-turbo | 100,277 | ~1.3 | ~1.8 | ~2.1 |
| o200k_base | GPT-4o | 200,019 | ~1.2 | ~1.7 | ~1.9 |
| Llama 3 | Llama 3, 3.1 | 128,256 | ~1.5 | ~2.1 | ~2.3 |
| GPT-2 | GPT-2, early GPT-3 | 50,257 | ~1.4 | ~2.0 | ~2.2 |
| BERT WordPiece | BERT, DistilBERT | 30,522 | ~1.5 | ~2.2 | ~2.5 |
These are empirical averages on representative corpora. Your specific text will vary. A sentence of common words (“the cat sat on the mat”) tokenizes efficiently. Technical documentation with camelCase identifiers, underscores, and punctuation tokenizes poorly.
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
texts = {
"simple": "the cat sat on the mat",
"technical": "def parse_json_response(response: str) -> dict:",
"mixed": "The API returns a JSON object with user_id and created_at fields.",
}
for name, text in texts.items():
tokens = enc.encode(text)
words = len(text.split())
print(f"{name}: {len(tokens)} tokens, {words} words, ratio={len(tokens)/words:.2f}")
# Output:
# simple: 10 tokens, 6 words, ratio=1.67
# technical: 23 tokens, 9 words, ratio=2.56
# mixed: 24 tokens, 13 words, ratio=1.85
Notice the simple sentence already exceeds 1.3 because “the” appears three times but each occurrence is a separate token. The ratio converges to the average only over larger samples.
Why this matters for engineering
Context window budgeting
If you’re building a RAG system with a 128k context window and you estimate 1.3 tokens/word, you budget ~98k words. If your actual corpus (code, logs, multilingual) runs at 2.0 tokens/word, you fit 64k words — a 35% shortfall. This causes silent truncation or blown context limits in production.
Cost estimation
At $2.50/M input tokens (GPT-4o), a 10k-word document costs:
- 1.3 tokens/word → 13k tokens → $0.0325
- 2.0 tokens/word → 20k tokens → $0.0500
At scale, a 30% estimation error compounds. If you process 1M documents/month, that’s $17,500 vs $27,500 — a $10k/month variance from a bad heuristic.
Latency and throughput
Token count drives compute. The attention mechanism is O(n²) in sequence length (or O(n) with flash attention, but constant factors matter). A 2x token count increase means roughly 2x prefill latency and 2x KV cache memory. If you batch requests, the tokenizer ratio directly affects how many requests fit in a batch.
Output token budgeting
When you set max_tokens=4000, you’re setting a token budget, not a word budget. At 1.3 tokens/word, that’s ~3,000 words. At 2.0 tokens/word, it’s 2,000 words. If your prompt includes code or non-English text, the model hits the limit mid-sentence.
Estimating tokens without calling the API
You don’t need to hit the tokenizer endpoint for rough estimates. Use these heuristics:
def estimate_tokens(text: str, tokenizer: str = "cl100k_base") -> int:
"""Fast heuristic, within ~10% for English prose."""
import tiktoken
enc = tiktoken.get_encoding(tokenizer)
return len(enc.encode(text))
# Heuristic constants (English, cl100k_base)
TOKENS_PER_WORD = 1.3
TOKENS_PER_CHAR = 0.25 # 4 chars ≈ 1 token
TOKENS_PER_LINE_CODE = 15 # rough average for Python
def quick_estimate(text: str, content_type: str = "prose") -> int:
word_count = len(text.split())
if content_type == "prose":
return int(word_count * TOKENS_PER_WORD)
elif content_type == "code":
return int(len(text) * TOKENS_PER_CHAR)
elif content_type == "mixed":
# Weighted blend
return int(word_count * 1.5)
For production systems, always use the actual tokenizer. The heuristic is for capacity planning, not billing.
# Production: always use real tokenizer
def count_tokens_production(text: str, model: str) -> int:
import tiktoken
encoding_name = MODEL_TO_ENCODING[model] # your mapping
enc = tiktoken.get_encoding(encoding_name)
return len(enc.encode(text))
Common misconceptions
“One token ≈ 4 characters” is a rule of thumb, not a law
This holds for English prose with cl100k_base. It fails for:
- Code:
async def fetch_user_data(user_id: int) -> User:— 47 chars, 18 tokens (2.6 chars/token) - Chinese: “你好世界” — 4 chars, 4 tokens (1 char/token)
- Emoji: “🚀🎉🔥” — 3 chars, 9 tokens (3 tokens per emoji in cl100k_base)
- Whitespace-heavy text: indentation, newlines, tabs each consume tokens
“Token count equals word count for short words”
False. “a”, “I”, “the”, “and” are each one token. But “tokenization” → token + ization (2 tokens). “antidisestablishmentarianism” → 6+ tokens. Frequency matters more than length.
“All models use the same tokenizer”
Each model family has its own tokenizer trained on its own corpus with its own vocabulary size and merge rules. GPT-4 and Llama 3 tokenize the same sentence differently. You cannot share token counts across models.
import tiktoken
text = "The quick brown fox jumps over the lazy dog."
gpt4_enc = tiktoken.get_encoding("cl100k_base")
llama3_enc = tiktoken.get_encoding("llama3") # hypothetical, use actual
print(f"GPT-4: {len(gpt4_enc.encode(text))} tokens")
# GPT-4: 16 tokens
# Llama 3 would differ — different vocabulary, different merges
“Tokenizers are reversible”
Mostly, but not perfectly. BPE loses byte-level information at merge boundaries. Unicode normalization, whitespace handling, and special tokens create edge cases where decode(encode(text)) != text. Always test round-trips if you depend on exact reconstruction.
enc = tiktoken.get_encoding("cl100k_base")
# Round-trip failure cases
cases = [
"hello\u200bworld", # zero-width space
"café", # NFC vs NFD
"hello world", # double space
"👨👩👧👦", # ZWJ sequence
]
for case in cases:
tokens = enc.encode(case)
decoded = enc.decode(tokens)
print(f"Original: {repr(case)} → Decoded: {repr(decoded)} → Match: {case == decoded}")
“I can optimize prompts by counting words”
You optimize by counting tokens. A prompt rewrite that saves 50 words but introduces rare subwords may increase token count. Measure with the actual tokenizer.
# Prompt optimization: measure tokens, not words
original = "Please analyze the following code snippet and identify any potential bugs or performance issues."
rewritten = "Analyze this code for bugs and performance issues."
print(f"Original: {len(enc.encode(original))} tokens")
print(f"Rewritten: {len(enc.encode(rewritten))} tokens")
# Original: 19 tokens
# Rewritten: 13 tokens — 32% reduction
Tokenizer differences in practice
When you route requests across multiple models — say, GPT-4o for complex reasoning and Llama 3.1 70B for cost-sensitive tasks — the same prompt consumes different token budgets. If you’re building a gateway that normalizes max_tokens across providers, you need per-model token estimation.
# Cross-model token normalization (simplified)
MODEL_TOKEN_MULTIPLIERS = {
"gpt-4o": 1.0, # baseline: cl100k_base / o200k_base
"gpt-4-turbo": 1.0,
"llama-3.1-70b": 1.15, # ~15% more tokens for same text
"llama-3.1-8b": 1.15,
"claude-3.5-sonnet": 1.05, # similar to GPT-4
}
def normalize_max_tokens(requested_tokens: int, target_model: str) -> int:
"""Convert a token budget from baseline model to target model."""
multiplier = MODEL_TOKEN_MULTIPLIERS.get(target_model, 1.0)
return int(requested_tokens / multiplier)
# User asks for 4000 output tokens on GPT-4o
# On Llama 3.1 70B, that's ~3478 tokens for equivalent word count
This matters for n4n.ai’s routing layer when honoring client max_tokens directives across heterogeneous backends — the gateway translates the budget per model so the user gets consistent output length regardless of which provider serves the request.
When to measure vs. when to estimate
| Scenario | Approach |
|---|---|
| Capacity planning, cost modeling | Heuristic (1.3× words for English prose) |
| Prompt engineering, few-shot selection | Real tokenizer, iterate fast |
| Production request validation | Real tokenizer, enforce hard limits |
| Cross-model budget normalization | Real tokenizer per model, cache results |
| Streaming response truncation | Real tokenizer, count incrementally |
Cache token counts for repeated prompts. The tokenizer is fast (~100k tokens/ms in Python), but not free at high QPS.
# Token counting with caching for repeated prompts
from functools import lru_cache
import tiktoken
@lru_cache(maxsize=10000)
def cached_token_count(text: str, encoding_name: str) -> int:
enc = tiktoken.get_encoding(encoding_name)
return len(enc.encode(text))
# For streaming: incremental counting
def count_stream_tokens(chunks: list[str], encoding_name: str) -> int:
enc = tiktoken.get_encoding(encoding_name)
total = 0
for chunk in chunks:
total += len(enc.encode(chunk))
return total
Summary
- English prose: 1.2–1.5 tokens/word depending on tokenizer (cl100k_base ≈ 1.3, Llama 3 ≈ 1.5)
- Code: 1.8–2.5 tokens/word
- Non-Latin scripts: 1.5–3.0 tokens/word (Chinese ~1 char/token, Korean ~1.5, Japanese ~1.8)
- Always use the actual tokenizer for production decisions
- Heuristics are for planning, not enforcement
- Token count drives cost, latency, and context limits — measure what matters
The tokenizer is the first layer of your LLM pipeline. Treat it like one: version it, test it, monitor it, and never assume the ratio from last year’s model applies to today’s.