← Files ClickHouseARCHIVED FILE
skills/clickhouse-js-node-coding/reference/sessions.md
6.95 KB · Sep 30, 2026 · 22:50 UTC
# Sessions and Temporary Tables
> **Applies to:** all versions. `session_id` is a server-level concept; the
> client just forwards it on every request that names it.
## Answer checklist
When answering "temp table disappears between calls" / "how do I share a
session" / anything involving `session_id`:
- State plainly that **temporary tables and session-scoped state are tied
to a `session_id`** — without a stable `session_id` across calls, every
request gets a fresh server-side session and the temp table is gone.
- Set `session_id` via `crypto.randomUUID()` either on `createClient` or
per-call.
- Warn that **`session_id` on a global / module-static client is an
anti-pattern** in any concurrent app (Express, server actions, workers,
etc.) — concurrent requests will share the same session and trip
`"Session is locked by a concurrent client"`. Recommend a short-lived
per-workflow client or per-call `session_id` instead.
- If `session_id` is set on the client, also set `max_open_connections: 1`
to serialize calls and avoid the concurrent-session error.
- For ClickHouse Cloud or any load-balanced deployment: **explicitly
recommend replica-aware routing or a single-node hostname** as the
primary remedy when sessions are needed. Sessions are pinned to one
node; behind an LB, consecutive requests may land on different nodes
and the temp table will appear to vanish. "Just collapse the workflow
into one handler" or "use a non-temporary table" are valid fallbacks
but secondary — name the routing fix first.
## When you need a session
Use a `session_id` whenever multiple calls must share **server-side state**:
- `CREATE TEMPORARY TABLE` (the table only exists within its session).
- `SET <setting> = <value>` to apply for subsequent queries on the same
session.
- Any other server feature scoped per session (e.g., session-scoped
variables in newer ClickHouse versions).
## ⚠️ `session_id` and concurrency
ClickHouse **rejects concurrent queries within the same session** — if two
requests arrive at the server at the same time sharing the same `session_id`,
the second one gets an error like
`"Session is locked by a concurrent client"`. This has two practical
implications:
1. **Do not set `session_id` on a global / module-static client** that handles
concurrent requests (e.g., an Express app's shared client). Every
in-flight request would share the same session and collide under load.
2. **If you do set `session_id` on a client**, restrict its concurrency:
set `max_open_connections: 1` so at most one request is in flight at a
time, turning the pool into a serial queue. This is fine for a
dedicated per-workflow client but wrong for a shared application client.
The right pattern for application code: create a **short-lived client** (or
use per-request `session_id`) scoped to a single logical workflow, not to
the entire process.
## Per-client `session_id`
Appropriate when **one client handles exactly one sequential workflow** (a
script, a background job, a single user's session that you've already manually
serialized in the code).
```ts
import { createClient } from "@clickhouse/client";
import * as crypto from "node:crypto";
const client = createClient({
session_id: crypto.randomUUID(),
max_open_connections: 1, // safeguard against concurrent-session errors
});
await client.command({
query: "CREATE TEMPORARY TABLE temporary_example (i Int32)",
});
await client.insert({
table: "temporary_example",
values: [{ i: 42 }, { i: 144 }],
format: "JSONEachRow",
});
const rs = await client.query({
query: "SELECT * FROM temporary_example",
format: "JSONEachRow",
});
console.info(await rs.json());
await client.close();
```
## Session-level `SET` commands
`SET` only persists within a session. With `session_id` defined on the
client, every subsequent call inherits the change.
```ts
import { createClient } from "@clickhouse/client";
import * as crypto from "node:crypto";
const client = createClient({
session_id: crypto.randomUUID(),
max_open_connections: 1, // safe-guard against concurrent-session errors
});
await client.command({
query: "SET output_format_json_quote_64bit_integers = 0",
clickhouse_settings: { wait_end_of_query: 1 }, // ack before next call
});
const rs1 = await client.query({
query: "SELECT toInt64(42)",
format: "JSONEachRow",
});
// → 64-bit integers come back as numbers in this query
await client.command({
query: "SET output_format_json_quote_64bit_integers = 1",
clickhouse_settings: { wait_end_of_query: 1 },
});
const rs2 = await client.query({
query: "SELECT toInt64(144)",
format: "JSONEachRow",
});
// → 64-bit integers come back as strings again
await client.close();
```
> **`wait_end_of_query: 1` matters here.** Without it, a `SET` on one
> connection in the pool may not yet be applied when the next query lands
> on the same socket.
## Per-request `session_id`
You can also pass `session_id` on a single `query()` / `insert()` /
`command()` call to override (or set) it for that one request.
## ⚠️ Sessions and load balancers / ClickHouse Cloud
Sessions are bound to a **specific ClickHouse node**. If a load balancer in
front of ClickHouse routes consecutive requests to different nodes, the
temporary table / `SET` won't be visible — you'll get
`UNKNOWN_TABLE` / surprising results.
Mitigations (in order of preference):
- **For ClickHouse Cloud, use [replica-aware
routing](https://clickhouse.com/docs/manage/replica-aware-routing)** so
consecutive requests in the same session land on the same node. This is
the right primary fix when you need sessions in a Cloud deployment.
- Talk to a single node directly (e.g., a node-pinned hostname) when
routing isn't an option.
- As a fallback only: avoid sessions for cross-node workflows and persist
intermediate state in a regular (non-temporary) table instead. This
trades the session requirement away rather than fixing it — use it only
if replica-aware routing / single-node connections aren't available.
## Common pitfalls
- **Forgetting `session_id` and being surprised that
`CREATE TEMPORARY TABLE` "disappears."** Without a session, every request
may land on a different connection / server context.
- **Setting `session_id` on a shared application client.** Under concurrent
load, two in-flight requests will share the same session and one will fail
with `"Session is locked by a concurrent client"`. Use per-request
`session_id` or a dedicated short-lived client instead.
- **Reusing the same `session_id` across unrelated workflows.** A second
session-using consumer will trip over your temporary tables and `SET`
values. Generate a fresh UUID per logical session.
- **Leaving session state pinned for the lifetime of the process.** If
long-lived clients accumulate `SET` / temp-table state, consider creating
a short-lived sub-client with its own `session_id` for the unit of work.
- **Skipping `wait_end_of_query: 1` on `SET`** — race conditions between
`SET` and the next query can show up under load.
SHA-256: 1876fa36be88d99dc70d585bd6c9a97f7d12a1ee1e4339e844deb68d6b9496c2