# `ConduitMcp.Session.EtsStore`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/session/ets_store.ex#L1)

Default in-memory session store backed by a public, named ETS table.

Sessions are keyed by `session_id` (a random URL-safe base64 string
generated by `ConduitMcp.Session.generate_id/0` on first `initialize`)
and store an arbitrary metadata map plus `"created_at"` (millisecond
Unix timestamp).

## Ownership

The table is owned by a long-lived `ConduitMcp.EtsOwner` process
(`ConduitMcp.Session.EtsStore.Owner`) started from `ConduitMcp.Application`, so it
outlives the short-lived Bandit request process that happens to touch it
first. A table owned by a request process would die with that request,
taking every session with it — `Session.get/2` would return
`{:error, :not_found}` for a session id the client had just been handed.

`ensure_table/0` remains as a fallback for embedding contexts where the
`:conduit_mcp` application is not started (e.g. some test setups). It is
not the primary mechanism.

## Concurrency

The table is `:public, :set, read_concurrency: true, write_concurrency: :auto`.
Reads do not block writes; writers serialize per key. Suitable for production
single-node workloads. For multi-node deployments, implement a
`ConduitMcp.Session.Store` against your distributed backend — see
`guides/multi_node_sessions.md`.

## Bounds & cleanup

Two mechanisms keep the table bounded, and both are needed:

  * **Row cap.** `create/2` refuses to grow the table past
    `config :conduit_mcp, :sessions_max_rows` (default
    100000, `:infinity` disables it) and returns
    `{:error, :session_limit_reached}`. Sessions are created by
    unauthenticated `initialize` requests, so without a cap a supervised
    (i.e. immortal) table is an unbounded growth surface.

    Note this is a *global* bound, not a per-caller one: it refuses new
    sessions rather than evicting existing ones, so an unauthenticated flood
    cannot displace a live session, but it can deny new ones until the
    janitor ages the flood out. `ConduitMcp.Plugs.RateLimit` is the
    per-caller bound; configure it on any public-facing transport.
  * **TTL sweep.** `cleanup/1` prunes rows older than a TTL.
    `ConduitMcp.Session.Janitor` is started by `ConduitMcp.Application`
    against this store by default; disable it with
    `config :conduit_mcp, :session_janitor, false`, or tune it with
    `config :conduit_mcp, :session_janitor, ttl: ..., interval: ...`.

## Configuration

Reference the store from a transport's `:session` option:

    {ConduitMcp.Transport.StreamableHTTP,
     server_module: MyServer,
     session: [store: ConduitMcp.Session.EtsStore, ttl: :timer.minutes(60)]}

# `cleanup`

Removes sessions older than `ttl_ms` milliseconds.

# `create`

Creates a session, unless the table is already at its configured row cap.

Returns `:ok`, or `{:error, :session_limit_reached}`.

# `ensure_table`

Ensures the ETS table exists.

Normally a no-op: `ConduitMcp.Session.EtsStore.Owner` creates the table at
application start. Retained so the store still works when `:conduit_mcp`
is not running as an OTP application.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
