# How Usage and Cost Tracking Works Per User and Per Session in Claude Code Telegram

> Discover how Claude Code Telegram tracks usage and costs per user and session. Learn about its RateLimiter, Session model, and StorageFacade for detailed insights.

- Repository: [Richard A/claude-code-telegram](https://github.com/richardatct/claude-code-telegram)
- Tags: deep-dive
- Published: 2026-02-20

---

**The bot implements a three-layer tracking system using a RateLimiter for per-user token buckets and daily cost budgets, a Session model for per-session accumulation, and a StorageFacade for persistent SQLite records of all costs.**

The RichardAtCT/claude-code-telegram repository provides a Telegram bot interface for Claude AI that requires strict accounting of API expenses. The system tracks every token request and USD-equivalent cost at both the user and session level, enabling real-time budget enforcement, daily resets, and comprehensive reporting for bot operators.

## Per-User Cost Tracking via RateLimiter

The **RateLimiter** class in [`src/security/rate_limiter.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/security/rate_limiter.py) serves as the primary gatekeeper for per-user usage and cost tracking per user or per session. It maintains two critical data structures: a token bucket for request throttling and a cost tracker for budget enforcement.

### Token-Bucket and Cost Budget Enforcement

The rate limiter initializes a `cost_tracker: Dict[int, float]` that maps Telegram user IDs to their cumulative USD spend within the current budgeting window. When a request arrives, the async method `check_rate_limit(user_id, cost, tokens)` validates both token availability and budget capacity.

After passing token-bucket checks, the system records the cost via `_track_cost()`:

```python

# src/security/rate_limiter.py – line 69

def _track_cost(self, user_id: int, cost: float) -> None:
    self.cost_tracker[user_id] += cost
    logger.debug("Cost tracked", user_id=user_id, cost=cost,
                 total_usage=self.cost_tracker[user_id])

```

### Daily Budget Reset Mechanism

The `_maybe_reset_cost_tracker()` method (lines 94-104) automatically resets user budgets every 24 hours. It compares the current time against `cost_reset_time[user_id]` and clears the tracker when the interval expires:

```python

# src/security/rate_limiter.py – line 95

reset_interval = timedelta(hours=24)
if now - last_reset >= reset_interval:
    self.cost_tracker[user_id] = 0
    self.cost_reset_time[user_id] = now

```

### User Status Retrieval

The `get_user_status(user_id)` method (lines 30-48) returns a comprehensive status dump including a `cost_usage` dictionary with **current**, **limit**, **remaining**, and **utilization** fields. This powers the `/status` bot command and admin dashboards.

## Per-Session Cost Accumulation

While the RateLimiter handles real-time enforcement, the **Session** model in [`src/claude/session.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/claude/session.py) maintains the cumulative cost of individual Claude conversations.

### Session Model and Total Cost Field

The `Session` dataclass declares `total_cost: float = 0.0` as a persistent field. This value represents the sum of all Claude API calls within that specific session window.

### Real-Time Cost Updates

Whenever the Claude SDK returns a response, the session updates its running total:

```python

# src/claude/session.py – line 64

self.total_cost += response.cost

```

The `response.cost` value originates from [`src/claude/sdk_integration.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/claude/sdk_integration.py), which extracts the `total_cost_usd` field from Claude's API response metadata. When the session object is persisted via [`src/storage/session_storage.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/session_storage.py) (lines 73-84), this `total_cost` is written to the SQLite `sessions` table.

## Persistent Storage and Aggregation

The **StorageFacade** in [`src/storage/facade.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/facade.py) provides the durable layer for usage and cost tracking per user or per session, ensuring data survives bot restarts.

### Storage Facade Pattern

After a Claude call completes, the Facade updates both user and session records atomically:

```python

# src/storage/facade.py – lines 118-126

user.total_cost += response.cost
session.total_cost += response.cost
await self._update_user(user)
await self._update_session(session)

```

### Database Schema

The underlying SQLite schema, defined in [`src/storage/database.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/database.py) (lines 43-55), creates `total_cost REAL DEFAULT 0.0` columns for the `users`, `sessions`, and `messages` tables. The `User` and `Session` ORM models in [`src/storage/models.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/models.py) (lines 38 & 72) map these columns to Python attributes.

### Aggregated Reporting

For administrative oversight, `StorageFacade.dashboard()` (line 321) aggregates per-session totals into system-wide statistics. Additionally, `Repositories.get_total_costs(days)` (line 654) in [`src/storage/repositories.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/repositories.py) sums daily costs across all users for historical trend analysis, powering cost alerts and budget forecasting.

## Practical Implementation Examples

### Checking User Cost Usage Inside a Handler

```python

# Example: inside a Telegram handler

from src.security.rate_limiter import RateLimiter
from src.config.settings import Settings

settings = Settings()                     # loads env vars

rate_limiter = RateLimiter(settings)

async def handle_message(update, context):
    user_id = update.effective_user.id
    # Estimate the cost of the incoming message (≈ 0.01 USD)

    cost_estimate = 0.01
    allowed, msg = await rate_limiter.check_rate_limit(user_id,
                                                       cost=cost_estimate,
                                                       tokens=1)
    if not allowed:
        await update.message.reply_text(msg)   # informs about budget

        return
    # ... proceed with Claude call

```

### Adding Cost to a Session After a Claude Response

```python
from src.claude.session import Session
from src.claude.sdk_integration import ClaudeSDKManager

async def ask_claude(user_id, prompt):
    session = await Session.load(user_id)          # restores or creates

    sdk = ClaudeSDKManager()
    response = await sdk.query(prompt,
                               session_id=session.session_id)

    # response.cost comes from Claude’s total_cost_usd field

    session.total_cost += response.cost
    await session.save()                           # persists total_cost

    return response.text

```

### Fetching User-Level Cost Summary for Admin Console

```python
from src.storage.facade import StorageFacade

async def admin_cost_report():
    facade = StorageFacade()
    dashboard = await facade.dashboard()
    total = dashboard["total_cost"]                # overall system cost

    per_user = dashboard["stats"]["summary"]["total_cost"]
    return f"System spent ${total:.2f} total; current user spend ${per_user:.2f}"

```

## Summary

- **RateLimiter** enforces per-user cost budgets and token buckets in [`src/security/rate_limiter.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/security/rate_limiter.py), automatically resetting daily limits via `_maybe_reset_cost_tracker()`.
- **Session** objects accumulate conversation costs in real-time through the `total_cost` field in [`src/claude/session.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/claude/session.py), updated after every Claude SDK response.
- **StorageFacade** persists cumulative costs to SQLite via [`src/storage/facade.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/facade.py), updating both `User.total_cost` and `Session.total_cost` atomically after each API call.
- **Database schema** in [`src/storage/database.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/database.py) defines `total_cost REAL` columns for users, sessions, and messages, ensuring durability across bot restarts.
- **Administrative reporting** via `StorageFacade.dashboard()` and `Repositories.get_total_costs()` enables system-wide cost monitoring and historical trend analysis.

## Frequently Asked Questions

### How does the system reset daily cost budgets for users?

The `RateLimiter` class tracks reset timestamps in `cost_reset_time` and automatically clears the `cost_tracker` dictionary for a user when `_maybe_reset_cost_tracker()` detects that 24 hours have elapsed since the last reset. This logic runs during every rate limit check in [`src/security/rate_limiter.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/security/rate_limiter.py).

### What happens when a user exceeds their cost limit?

When `check_rate_limit()` detects that the user's cumulative cost exceeds the configured budget, it returns `(False, message)` where the message explains the budget exhaustion. The Telegram handler in [`src/bot/handlers/message.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/bot/handlers/message.py) receives this rejection and replies to the user with the budget warning, blocking the Claude API call.

### How is cost data stored persistently across bot restarts?

The `StorageFacade` in [`src/storage/facade.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/facade.py) writes cost updates to SQLite via the `_update_user()` and `_update_session()` methods. The underlying schema in [`src/storage/database.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/database.py) defines `total_cost REAL DEFAULT 0.0` columns in the `users`, `sessions`, and `messages` tables, ensuring that cumulative costs survive process restarts and can be queried historically via `Repositories.get_total_costs()`.

### Where does the per-session cost value originate?

Each Claude API response includes a `total_cost_usd` field that the `ClaudeSDKManager` in [`src/claude/sdk_integration.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/claude/sdk_integration.py) extracts and maps to `response.cost`. The `Session` object in [`src/claude/session.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/claude/session.py) then adds this value to its `total_cost` field, creating a running tally of the conversation's API expenses that persists until the session ends.