# How free-claude-code Implements Session Persistence Across Server Restarts in FastAPI

> Discover how free-claude-code achieves session persistence across server restarts using atomic JSON files and debounced writes in FastAPI. Learn about efficient state management.

- Repository: [Ali Khokhar/free-claude-code](https://github.com/Alishahryar1/free-claude-code)
- Tags: internals
- Published: 2026-04-24

---

**Session persistence in free-claude-code is implemented via a thread-safe `SessionStore` class that maintains all conversation state in an atomic JSON file, using debounced writes to minimize I/O and explicit flush operations during FastAPI lifespan shutdown hooks.**

The `free-claude-code` project, an open-source integration layer for Claude Code CLI, must maintain conversation continuity even when the FastAPI server restarts. Rather than using an external database, the project implements a lightweight persistence mechanism that serializes session trees, node mappings, and message logs to disk using a custom JSON-based store.

## The SessionStore Architecture

The core persistence logic resides in [`messaging/session.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/session.py) within the `SessionStore` class. This class manages all mutable conversation state through a centralized storage interface.

When instantiated, the store immediately attempts to hydrate itself from the existing JSON file:

```python
self.storage_path = storage_path                 # L25-L27

self._load()                                    # L42

```

The `_load()` method (lines 47-95) reads the JSON file and populates three critical in-memory structures: `_trees` (conversation hierarchies), `_node_to_tree` (reverse lookups), and `_message_log` (audit trails). If the file does not exist, the store initializes empty structures and waits for the first mutating operation.

## Thread-Safe Mutations and Debounced Writes

All write operations acquire a reentrant lock to prevent race conditions during concurrent updates. When the API calls `save_tree()`, `record_message_id()`, or `clear_all()`, the following sequence executes:

```python
with self._lock:                                 # L35-L42 (save_tree example)

    self._trees[root_id] = tree_data
    # ... additional mutations ...

    self._schedule_save()

```

The `_schedule_save()` method (lines 11-31) implements a **debounced write strategy** using `threading.Timer`. Rather than writing to disk on every mutation, the store waits `0.5` seconds (`_save_debounce_secs`) after the last change before flushing. This coalesces rapid updates—such as streaming message chunks—into a single I/O operation.

When the timer fires, `_save_from_timer` captures a complete snapshot of the current state and passes it to `_write_data`, which performs an atomic JSON dump:

```python

# Conceptual flow from _save_from_timer

snapshot = self._snapshot()
self._write_data(snapshot)  # Atomic json.dump operation

```

This debouncing mechanism is critical for performance, as CLI output often generates dozens of events per second.

## Graceful Shutdown Handling

To prevent data loss during unexpected restarts, the FastAPI application registers a lifespan context manager in [`api/app.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/app.py) that explicitly flushes pending writes:

```python
if message_handler and hasattr(message_handler, "session_store"):
    message_handler.session_store.flush_pending_save()   # L66-L71

```

The `flush_pending_save()` method (lines 48-56) cancels any active debounce timer and forces an immediate synchronous write. This ensures that even if the server receives a SIGTERM seconds after the last mutation, no in-memory state is lost.

## Restoring State After Server Restarts

When the server initializes, the same `SessionStore` instance reconstructs the conversation queues before accepting new requests. In [`api/app.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/app.py) (lines 19-34), the startup logic retrieves persisted trees and rehydrates the message handler:

```python
saved_trees = session_store.get_all_trees()                # L20-L22

if saved_trees:
    message_handler.replace_tree_queue(
        TreeQueueManager.from_dict(
            {"trees": saved_trees,
             "node_to_tree": session_store.get_node_mapping()},
            # ... additional parameters

        )
    )

```

### Cleaning Up Stale Nodes

During restoration, the system identifies nodes that were in **PENDING** or **IN_PROGRESS** states when the previous process died. These `cleanup_stale_nodes()` operations discard incomplete operations and write the corrected state back to the JSON file. This guarantees that the restored session represents a consistent snapshot, free from phantom partial operations.

## Atomicity and Durability Guarantees

The persistence mechanism relies on three architectural qualities to ensure reliability:

- **Atomic snapshots**: The `_write_data` method uses a single `json.dump` call, ensuring the file contains either the complete previous state or the complete new state—never a partial write.
- **Debounced consistency**: The 500ms debounce window prevents file corruption that could occur from overlapping concurrent writes.
- **Explicit checkpointing**: The lifespan shutdown hook acts as a forced checkpoint, eliminating the window of data loss that exists between the last debounced write and process termination.

## Practical Implementation Examples

### Initializing the Session Store

```python
from messaging.session import SessionStore
import os

data_dir = os.path.abspath("./workspace")
os.makedirs(data_dir, exist_ok=True)

store = SessionStore(storage_path=os.path.join(data_dir, "sessions.json"))

```

### Persisting a Conversation Tree

```python
tree_id = "root_123"
tree_data = {
    "root_id": tree_id,
    "nodes": {"node_a": {"content": "System initialization complete"}},
    "metadata": {"timestamp": "2024-01-15T10:30:00Z"}
}
store.save_tree(tree_id, tree_data)   # Triggers debounced save automatically

```

### Recording Message Associations

```python
store.record_message_id(
    platform="slack",
    chat_id="C01ABCXYZ",
    message_id="msg_456",
    direction="outgoing",
    kind="assistant"
)

```

### Forcing Immediate Persistence

```python

# Typically called automatically during shutdown, but available for manual use

store.flush_pending_save()

```

## Summary

- **JSON-based storage**: All session state resides in a single file (default: [`claude_workspace/sessions.json`](https://github.com/Alishahryar1/free-claude-code/blob/main/claude_workspace/sessions.json)) managed by the `SessionStore` class in [`messaging/session.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/session.py).
- **Debounced writes**: Mutations are coalesced using a 500ms `threading.Timer` to reduce I/O overhead while maintaining eventual consistency.
- **Graceful shutdown**: The FastAPI lifespan hook in [`api/app.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/app.py) calls `flush_pending_save()` to ensure zero data loss on restart.
- **Automatic recovery**: On startup, the store reloads conversation trees and cleans stale nodes, presenting a consistent state to the message handler.
- **Thread safety**: All mutations are guarded by `threading.RLock()`, making the store safe for concurrent API access.

## Frequently Asked Questions

### How does free-claude-code prevent data loss if the server crashes unexpectedly?

The `SessionStore` uses debounced writes that flush to disk every 500ms after the last mutation. While a crash could lose up to half a second of updates, the atomic JSON write ensures the file never contains corrupted partial data. Additionally, the FastAPI lifespan context attempts to flush pending saves on SIGTERM, covering most graceful restart scenarios.

### Where is the session data physically stored?

Session data is stored in a JSON file specified by the `storage_path` parameter of `SessionStore`, typically located at [`settings.claude_workspace/sessions.json`](https://github.com/Alishahryar1/free-claude-code/blob/main/settings.claude_workspace/sessions.json) relative to the application root. This path is configurable during store initialization in [`api/app.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/app.py).

### Is the session store safe for concurrent requests?

Yes. The `SessionStore` wraps all mutating operations with `self._lock` (a `threading.RLock`), ensuring thread-safe access across multiple FastAPI workers or async handlers. The debounced save mechanism further prevents race conditions during disk writes by serializing access through the timer callback.

### What happens to in-progress Claude CLI operations when the server restarts?

Nodes in **PENDING** or **IN_PROGRESS** states are identified during the startup restoration process in [`api/app.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/app.py) and passed to `cleanup_stale_nodes()`. These incomplete operations are purged from the tree structure, and the cleaned state is written back to the JSON file, ensuring the system resumes from a consistent state.