Users
A user id rides on most production traces, and until now the store could filter by it, erase by it, and nothing else. "Who are my heaviest users", "when did this one last show up", "what does this account cost me" needed a scan of every trace the project holds — the scan the statistics rollup exists to avoid.
So users get a rollup of their own: the same hourly grain, one dimension over. A listing of who is there, a page for each of them, and the same charts and breakdowns the dashboard draws, restricted to one account.
What a user is
Any trace that carries a user id, which the mapper claims from
langfuse.user.id or user.id:
There is nothing else to configure. A trace that names no user belongs to no
user here — there is no "anonymous" pseudo-row, because the trace listing with
no user_id filter already is that view.
What is counted
Every number counts traces, not observations — the same rule the session roll-up follows:
| Field | Means |
|---|---|
traces |
How many traces are attributed to this user |
error_count |
How many of those traces carry at least one failed observation |
total_cost |
Summed over the traces whose client reported a cost; absent when none did, which is not the same as zero |
tokens |
The five token classes summed over this user's traces — input, output, cache read, reasoning, cache write — each absent when nothing reported it (api.md) |
sessions |
How many of this user's sessions have begun |
first_seen, last_seen |
The hours the rollup holds for them, at either end |
sessions counts a session where it starts — in the hour of the earliest
trace this user filed under that session id. A distinct count does not merge
across hours and a start does, so a sum over any range is exact: two hours of
sessions added together is the number of sessions begun in those two hours,
never a double count of one that spanned both.
Latency is on GET /api/v1/users/{id} only, as latency_ms.p50 / p95. Like
every percentile in this API it is histogram-based: accurate to a few percent,
stable across the expiry of the rows it came from.
The lag, and where there is none
The listing is answered from the rollup and nothing else. It therefore
trails live traffic by up to twice TRACEPAD_ROLLUP_INTERVAL — five minutes by
default, so ten in the worst case — exactly as the statistics do
(api.md). A user first seen a minute ago
is not on it yet.
Sorting and paging two sources on every page view would be the scan this
design removes, through a side door. One user is another matter: GET
/api/v1/users/{id} merges the live tail — the traces past the rollup's
watermark — so it is exact for any id, listed or not, and last_seen is exact
to the second when the tail holds them. An id nothing was ever filed under is
a 404.
The web interface says the same thing in the place it matters: the empty listing does not claim there are no users, it names the lag and links to the Traces screen, which is live.
On an upgrade
An existing install already has its statistics rolled up to now, so nothing about the history would look "changed" and these two tables would stay empty — or, on the upgrade that taught them the token classes, without tokens (until that walk is through, a user's tokens grow hour by hour as it re-rolls them). The migration therefore asks the aggregator to walk the rolled history once: the first pass after the upgrade re-rolls every hour it holds, which fills the per-user tables and rewrites the identical statistics rows. It is background work, it happens once, and the statistics keep answering from the rollup throughout.
An hour older than the project's retention_days is frozen
(retention.md), and the freeze is asked of
this table's own rows: an hour it already holds is left alone, and an hour it
holds nothing for is rolled from whatever raw rows are still there. Ordinarily
there are none — that is what the window means — so such a user's first_seen
starts where their retained traces do. Where the raw rows are still there,
which is history imported into an install that had already rolled those hours,
the pass fills them rather than leaving a permanent gap.
How much it costs
One row per (user, hour, environment, release, model), plus a trace-unit row
per (user, hour, environment, release). So the row count is
active user-hours × environments × (models + 1)
and nothing else — an inactive user costs nothing at all, and a user active in three hours of a day costs three hours of rows however many traces they ran in them.
In numbers, for one environment and two models, at 24 rolled hours per row and roughly 130 bytes a row (the five token classes are about 10 of them):
| Shape | Active user-hours a day | Rows a day | A month |
|---|---|---|---|
| 1,000 users, ~4 active hours each | 4,000 | 12,000 | ~47 MB |
| 100,000 users, ~2 active hours each | 200,000 | 600,000 | ~2.4 GB |
| 1,000,000 users, ~1 active hour each | 1,000,000 | 3,000,000 | ~12 GB |
The summary table beside it is one row per user the project has ever seen: about 80 bytes, and about 50 more in each of the five indexes the listing sorts by — for ids of ten characters or so, a million users is ~330 MB, once, not per day.
Two knobs bound all of it. stats_retention_days sweeps these rows on the
same schedule as the statistics rollup (retention.md), and a
deployment that emits a user id per request rather than per account is
paying for exactly that choice, linearly — the same bargain unbounded release
strings make in the statistics.
GET /api/v1/system counts both tables (users_hourly, users) so the size
is a number rather than a guess.
Retention and erasure
- Retention. The summary is over what the rollup retains: when
stats_retention_dayssweeps a user's oldest hours,first_seenmoves forward and the totals shrink with it. A user whose last row goes leaves the listing entirely. - Erasure.
DELETE /api/v1/projects/{id}/users/{user_id}/datadeletes the user's rows in both tables outright, with each chunk of their traces the erasure deletes — see admin.md. They are about the user, so they go rather than being recomputed: for an hour past the trace retention window there is nothing left to recompute them from. The project-widestats_hourlyfor such a frozen hour goes on counting the traces, which is the archive's documented position (retention.md).
The same user id in two environments is one user: one row, with the split visible in the page's environment breakdown.
From the command line
tracepad users ls # by last seen
tracepad users ls --sort cost --limit 20 # the expensive ones
tracepad users ls --sort tokens # the heavy ones, when nothing reports a cost
tracepad users ls --prefix acme: # case-sensitive prefix
tracepad users show user-4821
tracepad stats --user user-4821 --group-by day --since 168h
users ls walks with --cursor, --oldest and --newer like every other
listing, and --total adds the capped count. --json prints the endpoint's
own bytes. Full flags: cli.md.
From an agent
Three MCP tools, each one endpoint:
list_users— who is heaviest, most expensive, most error-prone, or here lately.get_user— one account's totals, exact including traffic too recent for the listing.get_statswithuser_id— that account over time, or split by model or environment.
See mcp.md.
In the web interface
Users sits between Sessions and Prompts, joining the screens about who: a user is a set of sessions, and their page is the dashboard's charts for one of them. See ui.md.