Manage Chat Sessions

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 63 of 200

What a Chat Session Is

A chat session is one separate conversation. Users of chat applications expect to have several: one about a Django bug, another about interview preparation, a third about a database design. Each session has its own history, so topics do not mix, and each can be resumed, renamed, or deleted independently.

Session management covers:

OperationExample
Create"New chat" button
ListThe sidebar of previous chats, newest first
Open / switchClicking a chat in the sidebar
RenameGiving a chat a meaningful title, manually or automatically
DeleteRemoving a chat the user no longer needs
Clean upRemoving old, inactive sessions according to a retention policy
ProtectEnsuring users can only access their own sessions

Lesson 62's conversations table already has a user_id and a title. This lesson builds a SessionManager on top of it.


Session IDs

Each session needs a unique identifier. Use random, unguessable IDs, such as UUIDs:

import uuid

session_id = uuid.uuid4().hex   # e.g. '3f9c2a7e41d84b6f9c1e0a5b7d2f8c61'
ID styleExampleProblem or benefit
Sequential integers1, 2, 3Easy to guess; users could try other numbers
Random UUIDs3f9c2a7e…Practically impossible to guess

Unguessable IDs are a useful extra layer, but they are not a replacement for ownership checks. Every operation must still verify that the session belongs to the current user.


Ownership Checks

Every session operation must confirm that the session belongs to the user making the request. Without this, a user who obtains or guesses another session's ID could read or modify someone else's conversation. This is an insecure direct object reference (IDOR) vulnerability (Lesson 25).

The check is simple: load the session's owner and compare it with the current user. If the session does not exist or belongs to someone else, respond with the same "not found" error. Using the same error for both cases avoids revealing which session IDs exist.


Automatic Titles

A sidebar full of "New chat" entries is not helpful. A common pattern is to generate a short title from the first message, using a small, fast model such as Claude Haiku 5.5:

First message: "My Django view returns a 500 error when the form is empty"
Generated title: "Django Empty Form 500 Error"

Title generation is a convenience, so it must never break the chat: if it fails, fall back to the first words of the message.


Retention and Cleanup

Stored conversations contain user data, so decide how long to keep them. A simple policy deletes sessions with no activity for a set number of days. Run cleanup periodically, for example once a day from a scheduled job. Tell users about your retention policy.


Practical Example: A Session Manager

sessions.py

"""sessions.py: manage multiple chat sessions per user on top of HistoryStore."""

import time
import uuid

import anthropic
from anthropic import Anthropic

from chatbot_core import BotConfig, ChatBot, Reply
from history_store import HistoryStore

TITLE_MODEL = "claude-haiku-5-5"


class SessionNotFound(Exception):
    """The session does not exist, or does not belong to this user."""


class SessionManager:
    def __init__(self, store: HistoryStore, client: Anthropic, config: BotConfig):
        self.store = store
        self.db = store.db
        self.client = client
        self.config = config

    def create(self, user_id: str) -> str:
        session_id = uuid.uuid4().hex
        self.store.ensure_conversation(session_id, user_id)
        return session_id

    def list_sessions(self, user_id: str) -> list[tuple[str, str, float]]:
        return self.db.execute(
            "SELECT id, title, updated_at FROM conversations WHERE user_id = ? ORDER BY updated_at DESC",
            (user_id,),
        ).fetchall()

    def open(self, session_id: str, user_id: str) -> ChatBot:
        self._check_owner(session_id, user_id)
        return ChatBot(self.client, self.config, history=self.store.load(session_id))

    def save_turn(self, session_id: str, user_id: str, reply: Reply) -> None:
        self._check_owner(session_id, user_id)
        if reply.ok:
            self.store.append(session_id, reply.new_messages)

    def rename(self, session_id: str, user_id: str, title: str) -> None:
        self._check_owner(session_id, user_id)
        title = title.strip()[:80] or "New chat"
        with self.db:
            self.db.execute("UPDATE conversations SET title = ? WHERE id = ?", (title, session_id))

    def delete(self, session_id: str, user_id: str) -> None:
        self._check_owner(session_id, user_id)
        with self.db:
            self.db.execute("DELETE FROM conversations WHERE id = ?", (session_id,))

    def auto_title(self, session_id: str, user_id: str, first_message: str) -> str:
        prompt = (
            f"<message>\n{first_message[:2000]}\n</message>\n\n"
            "Write a short title (at most 6 words) for a chat that starts with the message above. "
            "Reply with only the title."
        )
        try:
            response = self.client.messages.create(
                model=TITLE_MODEL, max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )
            title = "".join(b.text for b in response.content if b.type == "text").strip().strip('"')
        except anthropic.APIError:
            title = ""
        title = title or first_message[:40]
        self.rename(session_id, user_id, title)
        return title

    def cleanup(self, older_than_days: float) -> int:
        cutoff = time.time() - older_than_days * 86_400
        with self.db:
            cursor = self.db.execute("DELETE FROM conversations WHERE updated_at < ?", (cutoff,))
        return cursor.rowcount

    def _check_owner(self, session_id: str, user_id: str) -> None:
        row = self.db.execute("SELECT user_id FROM conversations WHERE id = ?", (session_id,)).fetchone()
        if row is None or row[0] != user_id:
            raise SessionNotFound(session_id)

Using the Session Manager

"""demo_sessions.py: create, title, list, resume, and protect chat sessions."""

from anthropic import Anthropic

from chatbot_core import BotConfig
from history_store import HistoryStore
from sessions import SessionManager, SessionNotFound

manager = SessionManager(HistoryStore("chat.db"), Anthropic(),
                         BotConfig(system="You are a concise programming tutor."))
USER = "student-1"

for first_message in ["My Django view crashes when the form is empty.",
                      "How should I prepare for a Python interview?"]:
    session_id = manager.create(USER)
    bot = manager.open(session_id, USER)
    reply = bot.send(first_message)
    manager.save_turn(session_id, USER, reply)
    manager.auto_title(session_id, USER, first_message)

print("Sessions (newest first):")
for number, (session_id, title, _) in enumerate(manager.list_sessions(USER), start=1):
    print(f"  {number}. {title}")

oldest_id = manager.list_sessions(USER)[-1][0]
bot = manager.open(oldest_id, USER)
reply = bot.send("What did I say was crashing?")
manager.save_turn(oldest_id, USER, reply)
print(f"\nResumed oldest session: {reply.text}")

try:
    manager.open(oldest_id, "someone-else")
except SessionNotFound:
    print("Another user cannot open this session.")

Example output:

Sessions (newest first):
  1. Preparing for Python Interviews
  2. Django View Crash on Empty Form

Resumed oldest session: You said your Django view crashes when the form is empty.
Another user cannot open this session.

How It Works

  1. create() generates a random UUID and creates the conversation row with its owner, using ensure_conversation() from Lesson 62.
  2. list_sessions() returns (id, title, updated_at) for the user's sessions, newest activity first. That is exactly what a sidebar needs. The method is named list_sessions rather than list to avoid hiding Python's built-in list.
  3. open() loads the session's stored history into a new ChatBot (Lesson 61), after checking ownership.
  4. save_turn() appends a successful turn's new_messages, which also updates updated_at, so active sessions move to the top of the list.
  5. _check_owner() runs before every operation that touches an existing session. Missing sessions and other users' sessions raise the same SessionNotFound error.
  6. auto_title() asks a fast model for a title of at most six words, removes surrounding quotes, and falls back to the first 40 characters of the message if the request fails or returns nothing. A failure never breaks the chat.
  7. rename() trims and limits titles to 80 characters, with "New chat" as the fallback.
  8. cleanup() deletes inactive sessions. Thanks to ON DELETE CASCADE, their messages are deleted too. cursor.rowcount reports how many sessions were removed.

Common Mistakes

Missing Ownership Checks

What happens: users can read or change other users' conversations by changing an ID.

Fix: check ownership in every operation, centrally, as _check_owner() does.

Guessable Session IDs

What happens: sequential IDs make it easy to probe for other sessions.

Fix: use random UUIDs, and check ownership.

Letting Title Generation Break the Chat

What happens: an API error while generating a title produces an error for the user's message.

Fix: treat titles as optional, with a fallback.

Keeping Conversations Forever

What happens: storage grows without limit, and old personal data accumulates.

Fix: define a retention policy, and run cleanup regularly.

Different Errors for "Missing" and "Not Yours"

What happens: attackers can tell which session IDs exist.

Fix: return the same "not found" error in both cases.


Best Practices

  • Use random, unguessable session IDs.
  • Check ownership on every session operation.
  • List sessions by recent activity.
  • Generate titles with a fast model, and fall back gracefully.
  • Delete sessions completely, including their messages.
  • Apply a retention policy, and communicate it to users.

Exercise

  1. Add a search(user_id, text) method that returns sessions whose title contains the given text.
  2. Add a message_count column to the output of list_sessions() using a LEFT JOIN with messages and GROUP BY.
  3. Write a small scheduled cleanup script that deletes sessions inactive for more than 90 days and prints how many were removed.
  4. Explain why save_turn() checks ownership even though open() already did.
Show answers for steps 2 and 4

Step 2:

def list_sessions(self, user_id: str) -> list[tuple[str, str, float, int]]:
    return self.db.execute(
        """
        SELECT c.id, c.title, c.updated_at, COUNT(m.id) AS message_count
        FROM conversations AS c
        LEFT JOIN messages AS m ON m.conversation_id = c.id
        WHERE c.user_id = ?
        GROUP BY c.id
        ORDER BY c.updated_at DESC
        """,
        (user_id,),
    ).fetchall()

LEFT JOIN keeps sessions that have no messages yet. Their count is 0, because COUNT(m.id) ignores the missing rows.

Step 4: in a web application, open() and save_turn() usually run in different requests. The session ID for saving comes from the client again, so it could have been changed. Each operation must verify ownership on its own, rather than relying on a check made earlier.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Manage Chat Sessions and get answers drawn from it.

Signed-in readers only.