Skip to main content
Lesson 11 of 22 · Responses API: OpenAI Fundamentals

Conversation state and session management

Lesson 11 of 22 Updated July 29, 2026 Developer
On this page
  1. Conversation state and session management
  2. Chaining with previous_response_id
  3. Running multiple conversations
  4. When to take control of the history
  5. Surviving a restart
  6. Key takeaways

Conversation state and session management

The Responses API supports conversation state natively, which drastically simplifies maintaining context across turns. You no longer need to reconstruct the history for every request: the server handles it for you, and you only pass an identifier.

Chaining with previous_response_id

The mechanism boils down to a single field. Every response has an id; pass it in previous_response_id on the next call, and the model has access to everything that came before. In the exchange below, the third question—“How do I add them?”—makes no sense in isolation: chaining lets the model know we’re talking about fractions and that the user is a beginner.

from openai import OpenAI
client = OpenAI()

# First exchange
r1 = client.responses.create(
    model="gpt-5.6-terra",
    instructions="You are a patient math teacher.",
    input="I'd like to learn about fractions."
)
print(f"R1: {r1.output_text}")

# Second exchange — context is preserved automatically
r2 = client.responses.create(
    model="gpt-5.6-terra",
    input="Can you give me a concrete example?",
    previous_response_id=r1.id
)
print(f"R2: {r2.output_text}")

# Third exchange — the entire conversation is accessible
r3 = client.responses.create(
    model="gpt-5.6-terra",
    input="And how do I add them?",
    previous_response_id=r2.id
)
print(f"R3: {r3.output_text}")
# The model knows we've been talking about fractions from the start

Consider the difference from the previous approach. With Chat Completions, the conversation lived in a list you maintained yourself and resent in full on every turn, with costs growing as the exchange lengthened.

# OLD — you had to manage everything yourself
history = [
    {"role": "system", "content": "You are a math teacher."},
    {"role": "user", "content": "I'd like to learn about fractions."},
    {"role": "assistant", "content": "Sure! Fractions..."},
    {"role": "user", "content": "A concrete example?"},
    {"role": "assistant", "content": "Let's take a pizza..."},
    {"role": "user", "content": "How do I add them?"},
]
# Sent in full on every call = rising costs

The same exchange now reduces to a single reference:

# NEW — a single ID is enough
r = client.responses.create(
    model="gpt-5.6-terra",
    input="How do I add them?",
    previous_response_id="previous_response_id"
)
# The server manages the history for you

Running multiple conversations

As soon as your application serves multiple users, the question becomes: which identifier belongs to whom? The answer is a mapping table between the user and the latest response.id for their conversation. The manager below encapsulates this logic: it adds previous_response_id only if a session already exists, then stores the new identifier. Alice and Bob can thus converse in parallel without their contexts mixing, and “new session” simply means forgetting the current identifier.

class SessionManager:
    """Conversation session manager."""

    def __init__(self):
        self.sessions: dict[str, str] = {}  # user_id -> last_response_id

    def send_message(self, user_id: str, message: str,
                     instructions: str = "") -> str:
        """Sends a message in the user's session."""
        kwargs = {
            "model": "gpt-5.6-terra",
            "input": message,
        }

        # Add instructions if provided
        if instructions:
            kwargs["instructions"] = instructions

        # Chain with the previous response if it exists
        if user_id in self.sessions:
            kwargs["previous_response_id"] = self.sessions[user_id]

        response = client.responses.create(**kwargs)

        # Save the ID for the next turn
        self.sessions[user_id] = response.id

        return response.output_text

    def new_session(self, user_id: str):
        """Starts a new conversation for the user."""
        if user_id in self.sessions:
            del self.sessions[user_id]

# Usage
manager = SessionManager()

# User Alice
print(manager.send_message("alice", "Hello, tell me about Python."))
print(manager.send_message("alice", "What are its advantages?"))

# User Bob (independent conversation)
print(manager.send_message("bob", "Hello, tell me about Rust."))

# Alice continues her conversation about Python
print(manager.send_message("alice", "How do I get started?"))

When to take control of the history

Automatic chaining has a limit: you cannot modify what has already been said. If your application needs to summarize past turns, delete a message containing personal data, or re-inject a reformulated context, switch to structured input and manage the list yourself. You lose simplicity, but you gain full control—and the ability to track token inflation turn after turn.

def manual_conversation():
    """Conversation with manual history management."""
    history = []

    while True:
        user_input = input("You: ")
        if user_input.lower() == "quit":
            break

        history.append({"role": "user", "content": user_input})

        response = client.responses.create(
            model="gpt-5.6-terra",
            instructions="You are a concise assistant.",
            input=history
        )

        assistant_msg = response.output_text
        history.append({"role": "assistant", "content": assistant_msg})

        print(f"Assistant: {assistant_msg}")
        print(f"  (Cumulative tokens: {response.usage.input_tokens})")

# manual_conversation()

Surviving a restart

An in-memory dictionary disappears on the first redeployment, taking all active conversations with it. In production, persist the identifiers—the JSON file below illustrates the principle, which you can adapt to your database or Redis. Note that we also record the timestamp and token usage, two pieces of information used for expiration and cost tracking, respectively.

import json
from datetime import datetime

class PersistentSessionManager:
    """Persistent sessions with backup."""

    def __init__(self, db_path: str = "sessions.json"):
        self.db_path = db_path
        self.sessions = self._load()

    def _load(self) -> dict:
        try:
            with open(self.db_path, "r") as f:
                return json.load(f)
        except FileNotFoundError:
            return {}

    def _save(self):
        with open(self.db_path, "w") as f:
            json.dump(self.sessions, f, indent=2)

    def send(self, session_id: str, message: str) -> str:
        kwargs = {"model": "gpt-5.6-terra", "input": message}

        if session_id in self.sessions:
            kwargs["previous_response_id"] = self.sessions[session_id]["last_id"]

        response = client.responses.create(**kwargs)

        self.sessions[session_id] = {
            "last_id": response.id,
            "updated_at": datetime.now().isoformat(),
            "total_tokens": response.usage.total_tokens
        }
        self._save()

        return response.output_text

Finally, implement expiration. Without cleanup, your session table only grows, and a user returning three weeks later would resume a conversation they no longer remember. Twenty-four hours is a reasonable starting point, adjustable based on your use case.

from datetime import datetime, timedelta

def clean_sessions(sessions: dict, max_age_hours: int = 24) -> dict:
    """Removes expired sessions."""
    now = datetime.now()
    active = {}

    for sid, data in sessions.items():
        updated = datetime.fromisoformat(data["updated_at"])
        if now - updated < timedelta(hours=max_age_hours):
            active[sid] = data

    deleted = len(sessions) - len(active)
    if deleted > 0:
        print(f"{deleted} expired session(s) removed")

    return active

To summarize these choices: use previous_response_id for simple conversations—it’s the most efficient; switch to structured input when you need to modify history; store response.id reliably, in a database rather than in memory; clean up inactive sessions; and monitor cumulative tokens, or billing will catch up with you.

Key takeaways

  • previous_response_id chains responses automatically without resending history
  • Every response has a unique id usable for chaining
  • Manage multiple sessions with a user_id -> response_id dictionary
  • Structured input (message list) offers more control over history
  • Implement session persistence and expiration in production