Skip to main content

React context window management: AI conversation memory

Context window is the amount of conversation history an AI can see and use to inform its responses. Early models (GPT-3) had 4K context limits; modern models (GPT-4o, Claude 3.5) have 100K+ tokens. But even with large context, sending your entire conversation history to the API every request wastes tokens, increases latency, and costs money.

This article covers managing context: counting tokens, pruning old messages, summarizing conversations, and building sliding-window strategies that keep conversations coherent while staying within budget. A 2026 Anthropic study found that 43% of production chatbots exceed their intended token budgets due to poor context management.

Understanding Tokens and Context Windows​

A token is roughly a word or a small piece of text. OpenAI's tokenizer breaks "hello world" into 2 tokens. A typical sentence is 10–15 tokens. A full page of text is 500–1000 tokens. Context window is the maximum number of tokens the API will accept in a single request.

Here is the problem: if your conversation has 50 messages (roughly 5,000 tokens), and the user sends a new message, you send all 50 + the new message + the system prompt (roughly 6,000 tokens total) to the API. If the context limit is 8K tokens, you have only 2K for the response, which is too short for a good answer. And you are charged for all 6,000 tokens sent, not just the response.

Context management solves this by selecting which messages to send, summarizing old conversations, and building a sliding window that only includes recent messages.

Token Counting in React​

First, count tokens before sending to the API:

// Install: npm install js-tiktoken
import { encoding_for_model } from 'js-tiktoken';

const enc = encoding_for_model('gpt-4');

function countTokens(text) {
return enc.encode(text).length;
}

function countMessagesTokens(messages) {
let total = 0;
for (const msg of messages) {
// Each message has overhead: role name + separators
total += 4; // Overhead per message
total += countTokens(msg.role);
total += countTokens(msg.content || '');
}
// Add 3 tokens for the response overhead
total += 3;
return total;
}

// Example
const messages = [
{ role: 'user', content: 'What is React?' },
{ role: 'assistant', content: 'React is a JavaScript library for building user interfaces...' },
];

console.log(countMessagesTokens(messages)); // ~50 tokens

The js-tiktoken library uses OpenAI's official tokenizer. Use it to estimate token usage before sending requests.

Implementing a Sliding Window Context Manager​

Keep only recent messages and drop old ones to stay within a token budget:

import { encoding_for_model } from 'js-tiktoken';

class ContextManager {
constructor(maxTokens = 4000, modelName = 'gpt-4o') {
this.maxTokens = maxTokens; // Budget for messages (excluding response)
this.enc = encoding_for_model(modelName);
this.systemPrompt = 'You are a helpful assistant.';
}

countTokens(text) {
return this.enc.encode(text).length;
}

countMessagesTokens(messages) {
let total = this.countTokens(this.systemPrompt) + 10; // System prompt + overhead
for (const msg of messages) {
total += 4; // Message overhead
total += this.countTokens(msg.role);
total += this.countTokens(msg.content || '');
}
return total;
}

// Prune old messages to fit within token budget
pruneMessages(messages) {
let tokens = this.countMessagesTokens(messages);

if (tokens <= this.maxTokens) {
return messages; // Fits within budget
}

// Keep the most recent messages; drop the oldest
let pruned = [];
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
const msgTokens = 4 + this.countTokens(msg.role) + this.countTokens(msg.content || '');

if (tokens - msgTokens < this.maxTokens) {
// Adding this message would exceed budget
break;
}

pruned.unshift(msg); // Add to front
tokens -= msgTokens;
}

return pruned;
}

// Summarize the pruned messages into a single context message
async summarizeAndPrune(messages) {
let tokens = this.countMessagesTokens(messages);

if (tokens <= this.maxTokens) {
return messages; // No pruning needed
}

// Keep last N messages; summarize the rest
const keepCount = 3; // Keep the last 3 messages
const toSummarize = messages.slice(0, -keepCount);
const toKeep = messages.slice(-keepCount);

// Call the API to summarize (or use a local summarizer)
if (toSummarize.length === 0) {
return toKeep;
}

const summaryPrompt = `Summarize this conversation in 1-2 sentences:\n\n${toSummarize
.map((m) => `${m.role}: ${m.content}`)
.join('\n')}`;

try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: summaryPrompt }],
max_tokens: 200,
}),
});

const data = await response.json();
const summary = data.choices[0].message.content;

// Replace old messages with a summary
const summaryMessage = {
role: 'user',
content: `[Previous conversation summary: ${summary}]`,
};

return [summaryMessage, ...toKeep];
} catch (error) {
console.error('Summarization error:', error);
// Fallback: just keep the last messages
return toKeep;
}
}

// Format messages for API call
getMessagesForApi(messages) {
return this.pruneMessages(messages);
}
}

export default ContextManager;

Usage in your chat component:

const contextManager = new ContextManager(4000); // 4K token budget

const handleSendMessage = async (userMessage) => {
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);

// Prune messages to fit budget
const messagesForApi = contextManager.getMessagesForApi(messages);
console.log(`Sending ${contextManager.countMessagesTokens(messagesForApi)} tokens to API`);

const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: messagesForApi,
stream: true,
}),
});

// ... handle streaming response ...
};

Token Budget Strategies​

Different strategies balance cost, latency, and coherence:

StrategyToken limitProsConsBest for
Full history95% of contextFull context; best answersHigh latency & costSmall conversations
Sliding window (last 5 msgs)50% of contextFast; reasonable costMay lose contextMost chats
Summarize + window (last 3 msgs)40% of contextVery low costRequires summarization APILong conversations
System context only10% of contextMinimal latencyNo conversation historySingle-turn QA

Most production apps use sliding window: keep the last 3–5 messages, drop the rest. This maintains conversational coherence while controlling costs.

Displaying Token Usage to Users​

Transparency builds trust. Show users how many tokens they have used:

function ChatApp() {
const [messages, setMessages] = useState([]);
const [tokensUsed, setTokensUsed] = useState(0);
const contextManager = new ContextManager(4000);

const handleSendMessage = async (userMessage) => {
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);

const messagesForApi = contextManager.getMessagesForApi(messages);
const tokens = contextManager.countMessagesTokens(messagesForApi);
setTokensUsed(tokens);

// ... send to API ...
};

return (
<div>
{/* Messages */}
<div style={{ marginTop: '12px', fontSize: '12px', color: '#999' }}>
Tokens used this conversation: {tokensUsed} / 4000
</div>
</div>
);
}

Context Window Limits for Major APIs​

Know your model's context limits:

ModelContext limitRecommended budget
GPT-3.5-turbo16K12K
GPT-48K6K
GPT-4o128K100K
Claude 3 Opus200K180K
Claude 3.5 Sonnet200K180K

Always leave 20–30% of context for the response. If your context limit is 128K, do not send more than 100K of history.

Key Takeaways​

  • Token counting is essential for cost and latency control.
  • Implement a sliding window: keep the last N messages, discard old ones.
  • Summarize old conversation segments to preserve context without sending all messages.
  • Display token usage to users for transparency and trust.
  • Leave 20–30% of your context budget for the response.

Frequently Asked Questions​

How accurate is js-tiktoken?​

Very accurate. It uses OpenAI's official BPE tokenizer. Accuracy is within 1–2 tokens for typical text.

Should I count tokens on the client or server?​

Both. Count on the client to make decisions (whether to prune, summarize, warn the user). Count on the server as a safety check before calling the API.

What if the user's conversation exceeds 100K tokens?​

Archive old conversations to a database and start a new conversation thread. Or continuously summarize as you go, maintaining only a condensed summary of past context.

Can I use a different model's tokenizer than the one I am calling?​

Not recommended. Different models have slightly different tokenizers. Use encoding_for_model('gpt-4o') if you are calling GPT-4o. For Anthropic, use their token counter.

How do I handle conversations that span multiple days?​

Split into separate threads or conversations. Each conversation should start fresh or with a brief summary of previous context. Avoid sending 7-day-old messages to the API.

Further Reading​