n4nAI

How much text fits in a 128k token context window?

A practical guide to estimating how much text fits in a 128k token context window, with runnable code for measuring and verifying token counts across models.

n4n Team3 min read649 words

Audio narration

Coming soon — every post will get a voice note here.

If you’re building on LLMs, you’ve asked yourself how much text is 128k tokens at least once. The answer depends on the tokenizer, the language, and the content itself — but you can measure it precisely for your use case. This guide walks through the steps to estimate, calculate, and verify context capacity for any model exposing a 128k window.

Step 1: Understand what a token actually represents

Tokens are not words. They’re subword units produced by a tokenizer (BPE, WordPiece, Unigram). English averages ~1.3 tokens per word. Code averages ~1.5–2 tokens per word due to punctuation and identifiers. Non-Latin scripts often run 2–4 tokens per character.

A 128k context window means the model can attend to 131,072 tokens total — prompt plus completion. If your prompt consumes 100k tokens, you have ~31k left for the response.

Quick reference for English prose:

Token budget Approximate words Approximate pages (250 words/page)
8k 6,150 25
32k 24,600 98
128k 98,400 394

These are rules of thumb. The only reliable number comes from running your actual text through the model’s tokenizer.

Step 2: Pick the right tokenizer for your model

Each model family uses a different tokenizer. Using the wrong one gives you systematically wrong counts.

Model family Tokenizer Vocab size Library
GPT-4 / GPT-4o / GPT-3.5 o200k_base 200,019 tiktoken
GPT-3 (davinci, etc.) p50k_base 50,257 tiktoken
Llama 3 / 3.1 Llama 3 tokenizer 128,256 transformers / tokenizers
Mistral / Mixtral Tekken 32,768 mistral-common
Gemma 2 Gemma tokenizer 256,000 transformers
Command R / R+ Cohere tokenizer 256,000 cohere

Install the relevant library:

# OpenAI models
pip install tiktoken

# Hugging Face models (Llama, Gemma, etc.)
pip install transformers tokenizers

# Mistral
pip install mistral-common

# Cohere
pip install cohere

Step 3: Count tokens programmatically

Here are minimal, runnable snippets for the most common model families.

OpenAI models (GPT-4o, GPT-4, GPT-3.5)

import tiktoken

def count_tokens_openai(text: str, model: str = "gpt-4o") -> int:
    """Return token count for an OpenAI model."""
    try:
        encoding = tiktoken.encoding_for_model(model)
    except KeyError:
        # Fallback for newer models not yet in tiktoken
        encoding = tiktoken.get_encoding("o200k_base")
    return len(encoding.encode(text))

# Example
text = "Your 128k context window holds approximately 98,000 English words."
print(f"Tokens: {count_tokens_openai(text, 'gpt-4o')}")

Llama 3 / 3.1 (and other Hugging Face models)

from transformers import AutoTokenizer

def count_tokens_hf(text: str, model_id: str = "meta-llama/Meta-Llama-3-8B") -> int:
    """Return token count for a Hugging Face model."""
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    return len(tokenizer.encode(text))

# Example
text = "Your 128k context window holds approximately 98,000 English words."
print(f"Tokens: {count_tokens_hf(text, 'meta-llama/Meta-Llama-3-8B')}")

Mistral / Mixtral

from mistral_common.tokens.tokenizers.mistral import MistralTokenizer

def count_tokens_mistral(text: str, version: str = "v3") -> int:
    """Return token count for Mistral models."""
    tokenizer = MistralTokenizer.from_version(version)
    return len(tokenizer.encode(text).tokens)

# Example
text = "Your 128k context window holds approximately 98,000 English words."
print(f"Tokens: {count_tokens_mistral(text, 'v3')}")

Cohere Command R / R+

import cohere

def count_tokens_cohere(text: str, model: str = "command-r-plus") -> int:
    """Return token count for Cohere models."""
    client = cohere.Client()  # Requires COHERE_API_KEY
    response = client.tokenize(text=text, model=model)
    return len(response.tokens)

# Example
text = "Your 128k context window holds approximately 98,000 English words."
print(f"Tokens: {count_tokens_cohere(text, 'command-r-plus')}")

Step 4: Account for chat formatting overhead

Raw text counts aren’t enough. Chat templates add special tokens for roles, message boundaries, and system prompts. These consume budget.

OpenAI chat format overhead

Each message adds ~4 tokens (role + formatting). The final assistant priming adds ~3 tokens.

import tiktoken

def count_chat_tokens_openai(messages: list[dict], model: str = "gpt-4o") -> int:
    """Count tokens for a full chat conversation including formatting."""
    encoding = tiktoken.encoding_for_model(model)
    tokens = 0
    for msg in messages:
        # Each message: <|im_start|>{role}\n{content}<|im_end|>\n
        tokens += 4  # overhead per message
        for key, value in msg.items():
            tokens += len(encoding.encode(str(value)))
    tokens += 3  # assistant priming
    return tokens

# Example
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Summarize this document: " + "x" * 50000},
]
print(f"Chat tokens: {count_chat_tokens_openai(messages, 'gpt-4o')}")

Llama 3 chat template

from transformers import AutoTokenizer

def count_chat_tokens_llama3(messages: list[dict], model_id: str = "meta-llama/Meta-Llama-3-8B-Instruct") -> int:
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    # apply_chat_template adds special tokens; tokenize=False returns string
    rendered = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    return len(tokenizer.encode(rendered))

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Summarize this document: " + "x" * 50000},
]
print(f"Chat tokens: {count_chat_tokens_llama3(messages)}")

Step 5: Measure real-world content types

Token density varies wildly by content type. Run your actual data through the tokenizer.

import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4o")

samples = {
    "english_prose": " ".join(["The quick brown fox jumps over the lazy dog."] * 1000),
    "python_code": "def fibonacci(n):\n    a, b = 0, 1\n    for _ in range(n):\n        yield a\n        a, b = b, a + b\n" * 500,
    "json_data": '{"users": [' + ','.join([f'{{"id":{i},"name":"User {i}","email":"user{i}@example.com"}}' for i in range(1000)]) + ']}',
    "chinese_text": "这是一个测试句子。" * 2000,
    "mixed_code_comments": "// Process user data\nfunction processUser(user) {\n  // Validate input\n  if (!user || !user.id) return null;\n  // Transform\n  return {\n    id: user.id,\n    name: user.name.toUpperCase(),\n    active: true\n  };\n}\n" * 300,
}

for name, text in samples.items():
    tokens = len(encoding.encode(text))
    words = len(text.split())
    ratio = tokens / words if words else 0
    print(f"{name:20s} | {tokens:>7,} tokens | {words:>7,} words | {ratio:.2f} tokens/word")

Typical output on GPT-4o tokenizer:

english_prose        |   16,243 tokens |   11,000 words | 1.48 tokens/word
python_code          |   28,912 tokens |   10,500 words | 2.75 tokens/word
json_data            |   42,105 tokens |    8,000 words | 5.26 tokens/word
chinese_text         |   24,000 tokens |    8,000 chars | 3.00 tokens/char
mixed_code_comments  |   18,450 tokens |    7,200 words | 2.56 tokens/word

JSON and code are token-expensive. Chinese characters average ~2–3 tokens each. Plan accordingly.

Step 6: Calculate your effective context budget

Subtract all fixed overhead from 128k to get your usable budget.

def calculate_budget(
    model_context: int = 131072,
    system_prompt_tokens: int = 0,
    few_shot_examples_tokens: int = 0,
    chat_overhead_per_message: int = 4,
    num_messages: int = 0,
    reserved_completion_tokens: int = 4096,
) -> int:
    """Return usable input tokens after overhead."""
    overhead = (
        system_prompt_tokens
        + few_shot_examples_tokens
        + chat_overhead_per_message * num_messages
        + reserved_completion_tokens
    )
    return max(0, model_context - overhead)

# Example: GPT-4o with system prompt, 3-shot, 5-message conversation, 4k reserved for output
budget = calculate_budget(
    system_prompt_tokens=500,
    few_shot_examples_tokens=2000,
    num_messages=5,
    reserved_completion_tokens=4096,
)
print(f"Usable input tokens: {budget:,}")
# Output: Usable input tokens: 124,472

Reserve completion tokens explicitly. If you need 8k output, reserve 8k. The model will stop at the context limit mid-sentence otherwise.

Step 7: Implement runtime guardrails

Count tokens before sending requests. Fail fast rather than hitting a 400 error.

import tiktoken
from dataclasses import dataclass
from typing import Optional

@dataclass
class ContextBudget:
    model_context: int
    reserved_completion: int
    system_tokens: int = 0
    few_shot_tokens: int = 0
    message_overhead: int = 4

    @property
    def max_input_tokens(self) -> int:
        return self.model_context - self.reserved_completion - self.system_tokens - self.few_shot_tokens

    def check_fit(self, messages: list[dict], model: str = "gpt-4o") -> tuple[bool, int, int]:
        """Return (fits, used_tokens, remaining_tokens)."""
        encoding = tiktoken.encoding_for_model(model)
        used = 0
        for msg in messages:
            used += self.message_overhead
            for v in msg.values():
                used += len(encoding.encode(str(v)))
        used += 3  # assistant priming
        remaining = self.max_input_tokens - used
        return remaining >= 0, used, remaining

# Usage
budget = ContextBudget(
    model_context=131072,
    reserved_completion=4096,
    system_tokens=500,
    few_shot_tokens=2000,
)

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "x" * 100000},
]

fits, used, remaining = budget.check_fit(messages, "gpt-4o")
if not fits:
    raise ValueError(f"Prompt exceeds budget by {abs(remaining):,} tokens. Used: {used:,}, Limit: {budget.max_input_tokens:,}")
print(f"OK: {used:,} tokens used, {remaining:,} remaining")

Step 8: Handle streaming and dynamic contexts

In multi-turn conversations, you must truncate history to stay within budget. Implement a sliding window or summarization strategy.

def truncate_history(
    messages: list[dict],
    budget: ContextBudget,
    model: str = "gpt-4o",
    keep_system: bool = True,
    keep_last_n: int = 2,
) -> list[dict]:
    """Remove oldest messages until conversation fits."""
    encoding = tiktoken.encoding_for_model(model)
    
    def count_msgs(msgs: list[dict]) -> int:
        total = 0
        for m in msgs:
            total += budget.message_overhead
            for v in m.values():
                total += len(encoding.encode(str(v)))
        total += 3
        return total
    
    # Always keep system message if present
    system_msg = messages[0] if keep_system and messages and messages[0]["role"] == "system" else None
    conversation = messages[1:] if system_msg else messages[:]
    
    while conversation and count_msgs([system_msg] + conversation) > budget.max_input_tokens:
        # Remove oldest non-kept message
        if len(conversation) <= keep_last_n:
            break  # Can't truncate further without losing required messages
        conversation.pop(0)
    
    result = []
    if system_msg:
        result.append(system_msg)
    result.extend(conversation)
    return result

# Test
long_conversation = [
    {"role": "system", "content": "System prompt."},
] + [
    {"role": "user" if i % 2 == 0 else "assistant", "content": f"Message {i} " + "x" * 5000}
    for i in range(20)
]

truncated = truncate_history(long_conversation, budget, "gpt-4o")
fits, used, remaining = budget.check_fit(truncated, "gpt-4o")
print(f"Kept {len(truncated)} messages, {used:,} tokens, {remaining:,} remaining")

Step 9: Verify with actual API calls

Token counting is deterministic, but provider implementations can differ. Verify against the real API.

import os
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def verify_token_count(model: str, messages: list[dict]) -> dict:
    """Send a minimal completion to get actual usage from the API."""
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        max_tokens=1,  # Minimal completion
        temperature=0,
    )
    return {
        "prompt_tokens": response.usage.prompt_tokens,
        "completion_tokens": response.usage.completion_tokens,
        "total_tokens": response.usage.total_tokens,
    }

# Compare local count vs API count
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Count the tokens in this message."},
]

local_count = count_chat_tokens_openai(messages, "gpt-4o")
api_usage = verify_token_count("gpt-4o", messages)

print(f"Local count:  {local_count}")
print(f"API count:    {api_usage['prompt_tokens']}")
print(f"Difference:   {abs(local_count - api_usage['prompt_tokens'])}")

Expected difference: 0–10 tokens (usually 0). If you see systematic offsets, your chat template implementation differs from the provider’s.

Step 10: Monitor production token usage

Log token consumption per request. Alert on trends approaching limits.

import time
import logging
from dataclasses import dataclass, asdict
from typing import Optional

@dataclass
class TokenMetrics:
    request_id: str
    model: str
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int
    context_limit: int
    utilization_pct: float
    latency_ms: int
    timestamp: float

def log_token_metrics(metrics: TokenMetrics) -> None:
    """Log structured metrics for observability."""
    logging.info("token_usage", extra=asdict(metrics))
    
    # Alert if utilization > 90%
    if metrics.utilization_pct > 90:
        logging.warning(
            "high_context_utilization",
            extra={
                "request_id": metrics.request_id,
                "utilization_pct": metrics.utilization_pct,
                "remaining_tokens": metrics.context_limit - metrics.total_tokens,
            }
        )

# Wrapper for instrumented calls
def tracked_completion(client: OpenAI, model: str, messages: list[dict], **kwargs) -> tuple:
    request_id = f"req_{int(time.time() * 1000)}"
    start = time.perf_counter()
    
    response = client.chat.completions.create(model=model, messages=messages, **kwargs)
    
    latency_ms = int((time.perf_counter() - start) * 1000)
    usage = response.usage
    context_limit = 131072 if "128k" in model or model in ("gpt-4o", "gpt-4-turbo") else 8192
    
    metrics = TokenMetrics(
        request_id=request_id,
        model=model,
        prompt_tokens=usage.prompt_tokens,
        completion_tokens=usage.completion_tokens,
        total_tokens=usage.total_tokens,
        context_limit=context_limit,
        utilization_pct=round(usage.total_tokens / context_limit * 100, 2),
        latency_ms=latency_ms,
        timestamp=time.time(),
    )
    log_token_metrics(metrics)
    return response, metrics

Verification checklist

Before deploying a 128k-context workflow, confirm:

  • Token counts match API-reported usage within ±10 tokens for representative samples
  • Chat template overhead is accounted for (system prompt, few-shot, message formatting)
  • Completion budget is reserved and enforced client-side
  • Truncation strategy preserves required context (system prompt, recent turns)
  • Non-English content is measured with actual tokenizer, not estimated
  • Structured logging captures utilization per request
  • Alerts fire at 90% context utilization

How much text is 128k tokens for your workload?

The only honest answer: run your data through the tokenizer. The code above gives you everything needed to measure, budget, and guardrail a 128k context window in production.

Tagscontext-windowtoken-counttokenization

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All context window & context length posts →