Skip to content

Bug: setTimeout overflow for large session durations (>24.8 days) invalidates session after 1ms in Node.js #594

@cinchpin

Description

@cinchpin

Describe the bug

When a bearer access method is configured with DURATION FOR SESSION exceeding ~24.8 days (2^31 - 1 ms), the JS SDK sets a setTimeout internally for session expiry. Node.js overflows the 32-bit signed integer used for setTimeout and clamps it to 1ms:

TimeoutOverflowWarning: 7775940000 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.

The session is then silently expired after 1ms. Any await between two db.query() calls yields to the Node.js event loop, the 1ms timer fires, the session is invalidated, and the next query runs unauthenticated:

NotAllowedError: Anonymous access not allowed: Not enough permissions to perform this action

This is entirely invisible — no error is thrown at signin time, and the first query (which runs synchronously before any yield) succeeds normally.

Steps to reproduce

  1. Define a bearer access method with session duration > 24.8 days:
DEFINE ACCESS token_access ON DATABASE TYPE BEARER
    FOR RECORD
    DURATION FOR GRANT 90d, FOR TOKEN 90d, FOR SESSION 90d;
  1. Sign in with a bearer key over WebSocket:
const db = new Surreal();
await db.connect('ws://localhost:8000', { namespace: 'main', database: 'main' });
await db.signin({ namespace: 'main', database: 'main', access: 'token_access', key: bearerKey });
  1. Run two queries with any await between them:
// Query 1 — succeeds
const [auth1] = await db.query('RETURN $auth');
console.log(auth1); // "user:abc123"

// Any await yields to the event loop — the 1ms timer fires here
await Promise.resolve();

// Query 2 — fails with NotAllowedError
const [auth2] = await db.query('RETURN $auth');
// NotAllowedError: Anonymous access not allowed

Expected behaviour

$auth should be preserved for the lifetime of the configured session duration. A 90-day session should not expire after 1ms.

Minimal reproducer

import { Surreal } from 'surrealdb';

const db = new Surreal();
await db.connect('ws://localhost:8000', { namespace: 'main', database: 'main' });
await db.signin({ namespace: 'main', database: 'main', access: 'token_access', key: 'surreal-bearer-...' });

const [a1] = await db.query('RETURN $auth');
console.log('Before yield:', a1); // user:abc123

await Promise.resolve(); // ← single microtask yield — triggers the expired timer

const [a2] = await db.query('RETURN $auth');
console.log('After yield:', a2);
// NotAllowedError: Anonymous access not allowed ($auth is now NONE)

Node.js prints the warning at signin time:

TimeoutOverflowWarning: 7775940000 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.

7,775,940,000 ms = 90 days.

Three-scenario comparison

Scenario Between queries Result
A Nothing (sync) ✓ PRESERVED
B External new Surreal() open/close ✗ LOST
C setTimeout(resolve, 100) ✗ LOST
D await Promise.resolve() ✗ LOST

Scenario D proves it is not the second connection that causes the loss — any microtask yield is sufficient.

Root cause hypothesis

The SDK likely computes the session expiry duration in milliseconds and passes it directly to setTimeout(). For sessions > ~24.8 days the value exceeds 2^31 - 1, which Node.js clamps to 1ms. The fix would be to cap the timer at 2^31 - 1 ms (and reschedule if needed), or to avoid relying on a JS timer for session invalidation entirely.

SurrealDB version

surrealdb/surrealdb:v3.0.5

SDK version

surrealdb@2.0.3

Node.js version

v24 (also reproduced on v22)

Connection type

WebSocket (ws://)

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions