Skip to content
NEW

Introducing SurrealDB Studio, the new official app of SurrealDB

Learn more

1/3

Release 3.3

4 patch releases · Latest 3.3.0-beta.4 on Sep 9, 2026

The fourth 3.3 beta is the largest of the line: a reworked graph engine with new relation types and much faster traversals, access-control additions to DEFINE…

3.3.0-beta.4

PRE-RELEASE

Released on Sep 9, 2026

The fourth 3.3 beta is the largest of the line: a reworked graph engine with new relation types and much faster traversals, access-control additions to DEFINE ACCESS, commit-time locked reads, and a public, licence-enforcing SurrealDB Enterprise image on Docker Hub, alongside a round of storage, index, vector, and networking fixes. Everything new in 3.3 from v3.3.0-beta.1, v3.3.0-beta.2 and v3.3.0-beta.3 is included and not repeated here.

Items tagged (Enterprise) on a heading or [Enterprise] on a bullet refer to the SurrealDB Enterprise product. Everything else applies to community.

The traversal storage and execution path has been rebuilt, and a relation table gains two ways to keep an edge's data in the adjacency structure itself, so a filtered traversal can answer without loading the edge records at all.

  • Traversals over hub vertices get substantially faster. Adjacency is packed into blocks maintained by a background fold, multi-source fan-outs run concurrently and coalesce shared work, record resolves overlap cursor fetches, adjacency key scans use a zero-copy cursor path, and ORDER BY id ... LIMIT traversals stop early at the scan. The gain is largest over high-degree vertices and from many starting records, and count(->edge) degree counts are served straight from adjacency storage instead of materialising every edge.

  • LIGHTWEIGHT relations drop the storage cost of a pure adjacency edge. DEFINE TABLE follows TYPE RELATION IN person OUT person LIGHTWEIGHT creates record-less edges storing nothing beyond id, in and out. RELATE is idempotent and returns an edge synthesised on the fly, with the canonical id follows:[person:a, person:b]; traversals, scans, count(), ordering, point reads and projections synthesise it likewise, and deleting a vertex cascades to its edges. IN / OUT are required and ENFORCED is implied. Such a relation cannot carry fields, indexes or events, take a data clause or a custom edge id, be UPDATEd or INSERTed into, or be SCHEMAFULL, CHANGEFEED or DROP. They suit pure adjacency (follows, likes) where the edge needs no properties.

  • INLINE fields answer a filtered traversal without reading edge records. DEFINE FIELD score ON likes TYPE number INLINE embeds the field value into the edge's adjacency entry, with ALTER FIELD ... INLINE / ... DROP INLINE to change it later. A traversal whose predicate reads only inline fields - ->(likes WHERE score > 5) - is then answered from the adjacency payload, byte-identical to the record path. Only top-level, non-COMPUTED fields on an existing TYPE RELATION table qualify (not id / in / out, and not on LIGHTWEIGHT relations). A payload past its size cap spills to the edge record, and a predicate touching any non-inline field falls back to the record path.

  • A per-vertex cache serves adjacency and back-links without a key scan. DEFINE TABLE and ALTER TABLE accept INLINE EDGES <n> and INLINE REFERENCES <n> caps, 0 to 256, removed with DROP INLINE EDGES / DROP INLINE REFERENCES. These maintain a compact per-vertex sidecar cache of a table's edge adjacency and of its reverse-reference back-links, serving traversals and reverse-reference lookups without walking the full key range, with results identical to an uncached table. A zero cap disables the cache, and a vertex whose set outgrows the cap falls back to the key scan.

  • CONTEXT computes a permission's payload once per session instead of once per row. DEFINE ACCESS ... TYPE RECORD accepts an optional CONTEXT ( <expr> ), evaluated once at authentication time with $auth bound to the authenticated record and under that record's own permissions. Its whole result is frozen on the session and read back as $session.data, so a permission can filter on the precomputed payload - PERMISSIONS FOR select WHERE org IN $session.data - rather than re-running a subquery for every row. The snapshot lasts the life of the session, bounded by DURATION FOR SESSION, and is recomputed on re-authentication; $auth is untouched and keeps its usual per-row fetch semantics. The clause must be read-only and attaches only to a method that authenticates a record. ALTER ACCESS ... CONTEXT { ... } sets it on an existing method, ALTER ACCESS ... DROP CONTEXT removes it.

  • AUDIENCE rejects a token minted for a different service. DEFINE ACCESS ... TYPE JWT, and record access configured WITH JWT, accepts an optional AUDIENCE "aud1", "aud2" clause. When present, a presented token's aud claim - a single string or an array - must contain at least one configured value, and the claim becomes mandatory, so a token omitting it is rejected. Methods defined without the clause continue to ignore aud, so existing definitions are unaffected. It works at root, namespace and database level, and the configured audiences are surfaced by INFO.

SELECT ... FOR UPDATE takes a commit-time lock on the selected records, so a read-then-write sequence can no longer be overwritten by a concurrent writer between the two steps. The read runs on a writeable transaction and registers each fetched record for conflict detection; if another transaction writes one of them first, the enclosing transaction fails to commit. Targets must be specific record ids - a literal such as person:1, or a parameter or function resolving to one - so whole tables, ranges (person:1..5), count buckets (|person:3|) and subquery sources are rejected, as is combining the clause with VERSION, GROUP BY, SPLIT, or a LIMIT spanning more than one target. Any SELECT ... FOR UPDATE, including one nested inside a value expression, makes its enclosing statement write-classified. Supported on every storage backend: in-memory, RocksDB, SurrealKV, IndexedDB and TiKV.

SurrealDB Enterprise is now distributed as a public Docker Hub image, surrealdb/surrealdb-enterprise, so a deployment can pull it with no registry credential to provision or rotate, and the image enforces a licence to run. Supply the key with SURREAL_LICENSE_KEY or SURREAL_LICENSE_KEY_FILE; it is validated online against keygen.sh - Ed25519-signed, cached on disk as a re-verified attestation, and revalidated in the background - with grace windows so a keygen outage does not take a running node down. A valid licence runs; an expired one runs on a grace window; a suspended, over-limit or missing licence refuses to start.

  • [Query] Disjunctive and reachability filters now run at index speed. Top-level OR filters across indexes fuse into a single bitmap union, and graph-reachability sub-conditions inside a WHERE compile to bitmap branches, so both evaluate over index bitmaps rather than row by row.

  • [Query] A closure stored in a field can be called like a method. $obj.method() now falls back to calling a closure held in the object's method field when no built-in method of that name applies; built-ins such as len still take precedence, and a name that is neither still errors. Such closures also now execute with the session's namespace/database context, so a body that reads or writes the database works on every planner strategy, and a write inside one is classified read-write so it persists.

  • [Vector] A compaction backlog no longer inflates the memory a KNN search needs. HNSW and DiskANN searches bound how much pending, not-yet-compacted index data one search materialises, so the read path stays bounded however far behind compaction is. The in-memory HNSW graph is retained when a compaction pass fails, so vector search survives a failure, and a search that exceeds one materialisation batch emits a WARN naming the index.

  • [Storage] Reclaiming removed data no longer spikes memory, and picks up after a restart. Data left behind by removals - dropped indexes and the per-record doc-ID mappings they own, dropped tables, and change-feed history - is now reclaimed by a background task in bounded, resumable batches rather than one large operation. A REBUILD INDEX issued while the table's shared doc-ID space is still being reclaimed is refused with a message to retry.

  • [Storage] Less write amplification on bulk inserts and index builds. The internal document ids used by indexed writes, and the other internal surrogate keys the catalog assigns, are now committed once per reserved window instead of once per record. User-facing sequences (sequence::nextval) still hand out a dense, gap-free run of values while the node is running.

  • [Memory] The overload guard now refuses queries against an accurate memory figure. Tracked bytes record the size class the RocksDB allocator actually reserves rather than the requested size, so SURREAL_MEMORY_THRESHOLD measures what the process really holds. Four process-memory gauges (surrealdb_process_memory_allocated_bytes, _requested_bytes, _reserved_bytes, _threshold_bytes) let operators watch how close an instance is to refusing queries.

  • [Server] Health checks answer immediately, however slow the startup. The HTTP listener now binds before the datastore's startup transactions run, so /health responds straight away, with the remaining work behind a readiness gate during which requests receive 503 and /ready reports not-ready. The first archived-node cleanup and peer-expiry pass is likewise off the bind path, so an instance with many archived nodes still starts serving promptly.

  • [gRPC] Large records and transactions fit in a single gRPC message. SURREAL_GRPC_MAX_MESSAGE_SIZE bounds a message in both directions and is the figure advertised during the capability handshake, so it is set in one place; it defaults to the HTTP /rpc body limit (SURREAL_HTTP_MAX_RPC_BODY_SIZE). Raise it for a workload such as bulk ingest of embeddings. Too small a value is raised to a safe floor and an unparsable one falls back to the default. Clients get a matching GrpcConfig (surrealdb::opt::GrpcConfig::max_message_size, via Config::grpc(...)), and one that raises its limit above the server's advertised figure has oversized requests refused up front.

  • [gRPC] A gRPC failure now names its cause. Errors carry their structured kind and details, plus the backwards-compatible numeric code, so a fault such as an oversized message arrives as a status naming the limit rather than an opaque HTTP/2 stream reset.

  • [API] An oversized API body is rejected before it is decoded. DEFINE API gains an api::req::max_body(limit) middleware that answers HTTP 413 on a raw body exceeding limit, given as a non-negative byte count or a size string such as "1mb" or "512kb". Only raw bytes and string bodies are measured; place it ahead of api::req::body(...) so oversized payloads are never parsed.

  • [Query] An ordinary SELECT used as an inline subquery value - most visibly as a FOR range, FOR $r IN (SELECT ...) - now keeps its array shape regardless of row count: zero rows give [], one row gives [x]. The shape previously followed the row count, collapsing a single-row subquery to the bare element and an empty one to NONE.

  • [Query] Parentheses around an equal-precedence sub-expression are now preserved when an expression is serialised back to SurrealQL text, so a DEFINE FUNCTION or DEFINE FIELD body computes the same result after being reloaded. Previously RETURN $x / ($a * $b) was re-emitted without them and reparsed as ($x / $a) * $b.

  • [Query] SELECT count() now agrees with SELECT on which records a user may see. The count fast paths lacked the read bypass the row scan uses, so a table permission whose predicate read another table could deny every row and return 0 while SELECT returned them.

  • [Query] LIMIT / START are now kept above the sort when an ORDER BY is withheld from access-path selection because its sort field carries a field-level SELECT permission. The limit was previously pushed into the record-id-ordered scan, handing the sort a truncated prefix and returning rows out of order.

  • [Schema] A field whose name is a reserved keyword (for example function or select) now round-trips through the schema catalog, surviving later DEFINE FIELD statements on the same table and reading its stored data back. Stored field paths are re-parsed with the idiom grammar rather than as a general expression, so a leading keyword reads as a field name.

  • [Schema] A silo module defined by 3.2.4 or earlier is addressable by name again. 3.3 inserts :: before the version segment of a module's derived key, so silo::acme::widgets<1.2.3> written by an earlier release no longer matched the silo::acme::widgets::<1.2.3> the current build derives: REMOVE MODULE and ALTER MODULE reported it missing, calls to its exports could not reach it, and OVERWRITE wrote a second row, all while INFO FOR DB listed it correctly. Upgrading moves every mismatched row to the key its own value derives, carrying permissions, comments and the unsigned flag across. Only silo:: names were affected.

  • [Storage] On the in-memory (memory) backend, a transaction or function can now delete a record and create the same record id again within that transaction; the second create previously failed with a "key already exists" error because the engine checked the base store rather than the transaction's pending changes.

  • [WebSocket] Replies and live-query notifications now use separate outbound queues, each sized by SURREAL_WEBSOCKET_RESPONSE_CHANNEL_SIZE (minimum two), so a client that pipelines many requests without reading responses in lock-step is no longer mistaken for one that has stopped reading its notifications. A client that genuinely stops reading them is closed with a stated reason rather than buffered without bound.

  • [WebSocket] Connection teardown now completes the WebSocket close handshake (RFC 6455 5.5.1) by closing the socket rather than only flushing it, so a client that starts a close, or one closed by the server, sees a clean close instead of a ResetWithoutClosingHandshake transport error.

  • [Scripting] Reading response headers in a scripting fetch no longer aborts the process when a remote server returns a header value containing non-ASCII or non-UTF-8 bytes. The Headers accessors (entries, values, get, getSetCookie) decode such values lossily; valid ASCII and multi-byte UTF-8 values are returned unchanged.

  • [Index] Removing, replacing (DEFINE INDEX OVERWRITE) or rebuilding an index while writes or a CONCURRENTLY build are in flight now retires the old generation cleanly: the replaced index is no longer queryable through its replacement, no stale generation returns wrong results, and the retired data is cleaned up. Retirement is fenced on the transaction that removes the definition rather than on a datastore close.

  • [Index] An index that is being built or rebuilt is left to its dedicated builder, so background index compaction on any node no longer touches an index whose on-disk layout only that builder knows.

  • [Graph] Fixed the lazy migration of legacy graph edges: re-relating an edge stored in the old layout could leave a stale reverse key behind, so a traversal over that vertex returned the edge twice and the leftover key was never reclaimed. Both legacy vertex-side keys are now removed before the new ones are written.

  • [Index] Full-text and vector (HNSW, DiskANN) indexes built under 3.2 are read in their original 3.2 on-disk layout and remain queryable after upgrading, with no forced rebuild. Each keeps its legacy per-index doc-ID layout until you explicitly run REBUILD INDEX, which migrates it to 3.3's shared doc-ID layout.

  • [Graph] Existing graph edges are read and converted transparently as they are re-related or written, so no re-import or migration step is required. A background maintenance task - a "fold", by default every 5 seconds and tunable - packs adjacency into blocks for fast traversal.

  • [Enterprise] Running SurrealDB Enterprise now requires a licence key (SURREAL_LICENSE_KEY or SURREAL_LICENSE_KEY_FILE); a node with no valid, unexpired, in-limit licence refuses to start. The node reaches keygen.sh to validate, with an on-disk attestation cache and grace windows covering transient keygen unavailability.

Upgrade or install

Get SurrealDB v3.3.0-beta.4

Pick how you want to install or upgrade. SurrealDB Studio can update connected instances in place, or choose a platform below to copy a CLI command for v3.3.0-beta.4.

You can upgrade your SurrealDB Cloud instance to v3.3.0-beta.4 effortlessly through SurrealDB Studio.

  1. Select your organisation and instance
  2. On the dashboard, click on the "Upgrade" button
  3. Your instance will be updated and restarted automatically

3.3.0-beta.3

PRE-RELEASE

Released on Aug 20, 2026

The third 3.3 beta moves the permission and COMPUTED write rules to runtime enforcement, and fixes the embedded JavaScript engine's storage lifetime and packaging found while wiring the SDK against the published v3.3.0-beta.2 packages. Everything new in 3.3 from v3.3.0-beta.1 and v3.3.0-beta.2 is included and not repeated here.

Items tagged [Enterprise] on a bullet refer to the SurrealDB Enterprise product. Everything else applies to community.

  • [Query] Permission and COMPUTED write rules move to runtime enforcement. v3.3.0-beta.1 added a definition-time check that tried to decide, when a schema object was defined, whether a PERMISSIONS guard or COMPUTED body could ever reach a data-modifying statement. Seeing through a function call means resolving the callee's stored body against the catalog, and that answer is not sound: whether a body reaches its write depends on which branch its arguments select, so a function that conditionally calls CREATE / UPDATE was refused for every caller, including the guards and COMPUTED fields that never take that branch. That check is dropped: a PERMISSIONS clause loses its definition-time write refusal entirely, and a COMPUTED body loses it for writes reached through a function call. A write written directly in a COMPUTED body is still refused at DEFINE / ALTER time. A DEFINE / ALTER that stores a writing SELECT guard, or a COMPUTED body that reaches a write through a function call, is now accepted, and fails at the point a read actually reaches the write, with the existing A PERMISSIONS clause cannot contain a statement that modifies data / A COMPUTED clause cannot contain a statement that modifies data. The runtime frame is exact regardless of call depth or indirection (eval, scripts, closures arriving as data), which the definition-time check could never be. create / update / delete permission clauses may carry side effects, since they are reached only from a statement that is already writing. This reverses the definition-time behaviour described under Writing computed fields and functions are refused at definition time in v3.3.0-beta.1.

  • [Embedded] Per-platform binary packages for @surrealdb/node-native. The eight native binaries are no longer bundled inside the root package. Each is published as @surrealdb/node-native-<platform> carrying its own os / cpu / libc, and the root package lists them all under optionalDependencies, so an install downloads only the one binary its host can use rather than all eight, keeping a @surrealdb/node install under AWS Lambda's 250 MB unzipped limit.

  • [Embedded] free() on @surrealdb/node-native now releases the datastore's storage before it resolves, so a file-backed datastore can be reopened on the same path after close() within one process. surrealkv releases its lockfile late in shutdown and Drop only spawned a detached close, so free() previously returned with the data directory still locked. The wasm engine has no file backends and is unaffected.

  • [Embedded] The surrealkv+versioned:// scheme, which has named no backend since versioning moved to the ?versioned=true parameter, now reports the supported spelling instead of the generic "unable to load the specified datastore", which read like a build compiled without the backend.

  • [SDK] Query-stream begin frames now carry the protocol revision, and the structural contract a client can rely on when decoding is written down: a stream tag is never repurposed, unknown tags and fields are ignorable, and single is always present on a finished frame (it was already on the wire; only the SDK type made it optional).

  • The mutable_permissions experimental capability is removed. It existed only to reopen the create / update / delete guards that the definition-time rule had shut, and was on for every server already. Remove any --allow-experimental mutable_permissions / --deny-experimental mutable_permissions flag and the SDK's ExperimentalFeature::MutablePermissions builder entry; there is no replacement, since those guards now permit side effects unconditionally.

  • The surrealdb_statement_mutable_permission_writes_total metric is removed along with the capability. Drop any dashboard panel or alert that referenced it.

Newer patch available

Upgrade to 3.3.0-beta.4

You are viewing the 3.3.0-beta.3 changelog. A newer patch in this release line is available - we recommend running 3.3.0-beta.4 for the latest fixes and improvements.

View 3.3.0-beta.4 release notes

3.3.0-beta.2

PRE-RELEASE

Released on Aug 18, 2026

The second 3.3 beta is a focused round of startup, shutdown, and readiness hardening for clustered deployments, driven by incidents observed on production multi-node clusters. Everything new in 3.3 from v3.3.0-beta.1 is included and not repeated here.

Items tagged [Enterprise] on a bullet refer to the SurrealDB Enterprise product. Everything else applies to community.

  • [Server] Configurable readiness heartbeat window. The /ready probe treats a node as unhealthy once its cluster heartbeat goes stale, and that window can now be set directly with --readiness-heartbeat-max-age / SURREAL_READINESS_HEARTBEAT_MAX_AGE. Previously it was always derived as three times the node-membership refresh interval (9s by default), which welds the probe to the very write path it measures. That is fine for a storage engine whose node-row write is local and sub-millisecond, but under a distributed engine the same write is a consensus transaction whose latency moves with cluster health, so a slow-but-healthy write path made every replica flip NotReady at once and presented partial degradation as total unavailability. The default is unchanged, so behaviour is identical unless you set the flag. Startup now also warns if the configured window reaches the interval at which peers archive an unresponsive node (30s), since a node reported ready after its peers have written it off keeps taking traffic while its cluster registration and live queries are collected underneath it.

  • [Server] Graceful shutdown of startup background work. Deferred startup tasks - a startup import, root credential creation, the Surrealism eager module load, and the first node-maintenance pass - now register with the datastore, so shutdown waits for them (bounded by the existing 30-second maintenance shutdown timeout) instead of closing the storage engine while they are still writing. Cancellation is prompt: each task selects on the shutdown token, an aborted import deliberately leaves the node not ready rather than flipping a dying pod healthy, and unreached Surrealism modules simply load on first use.

  • [Server] Archived-node cleanup no longer gates the HTTP listener. On a cluster recovering from a restart storm, the listener could take minutes to become reachable after the storage engine was ready: the bind waited behind sequential startup operations, and remove_nodes opened one write transaction per archived node with no bound on how much dead-member residue a restart storm left behind. The first expiry and cleanup pass is now spawned after this node registers itself, not awaited, so it cannot hold up the bind. The periodic maintenance scheduler is unchanged.

  • [Server] The cluster heartbeat now survives a slow write. It previously made a single attempt per tick under a fixed 60s budget, so one slow write could lose readiness roughly 51s before the attempt itself failed. Each tick now spends its whole budget - bounded relative to the readiness window - on the write and retries a fast failure within the tick, so a write that is slow but working still lands in time to keep the node ready.

  • [Enterprise] The read-catch-up completeness watermark is now persisted, so a restart no longer re-drains the committed log from zero. The floor was held in memory only, so every process start walked the entire committed history - one quorum round trip per 100 records - under a begin whose fixed 300s request timeout it eventually exceeded; the startup gate treated that as fatal and exited, and the next start walked from the same zero floor, crashlooping. The watermark now lives in the durable meta column family, is restored on open clamped to the durable committed frontier, and is established at startup under a begin bounded on progress rather than wall time. Measured on a 3-node cluster: a rollout that had been crashlooping against a 30-minute timeout completed in 5m04s, the startup gate went from 300s-then-exit to single-digit milliseconds, and watermark-caused readiness 503s went to zero.

  • [Enterprise] A pending admission freeze no longer wedges startup. A guard-holding wait inside the read catch-up - the committed-manifest window pull, with 60s of patience per page in an unbounded page loop - could hold its guard across a Normal-leaving view transition, so the freeze never drained and the startup warm-up failed. Because that path exits the process on any error, the node killed itself and restarted into the same state. Those waits are now raced against the admission-freeze marker and bail retriably, and the warm-up absorbs the transients under a single request budget instead of surfacing them. Store replays and offloaded reads are deliberately not preempted, since dropping an offloaded closure would strand a durable write ahead of its post-apply bookkeeping.

  • [Enterprise] A begin or read establishment refused because a view transition is pending is now reported as a retryable TransactionConflict rather than a hard datastore error. The commit path deliberately keeps its unknown-outcome classification, since a commit interrupted mid-round cannot know whether other replicas still hold tentative reservations.

  • [Enterprise] If you applied SURREAL_DS_STARTUP_NORMAL_TIMEOUT=0 as a break-glass on v3.3.0-beta.1, unset it on this build. The startup warm-up now retries through view-transition churn, so =0 only removes protection - it disables both enterprise startup gates and leaves check_version as the only one.

  • [Enterprise] Keep SURREAL_STARTUP_OPERATION_TIMEOUT=180 for now. The default is still 60s, which is tight for a consensus engine on a cold cluster.

Newer patch available

Upgrade to 3.3.0-beta.4

You are viewing the 3.3.0-beta.2 changelog. A newer patch in this release line is available - we recommend running 3.3.0-beta.4 for the latest fixes and improvements.

View 3.3.0-beta.4 release notes

3.3.0-beta.1

PRE-RELEASE

Released on Aug 14, 2026

This is the first beta in the 3.3 series. It opens the line with two new ways to connect - a Postgres wire-protocol listener that lets any Postgres client run SurrealQL or ISO GQL, and a gRPC engine with end-to-end result streaming - and makes ISO GQL available by default. File buckets gain S3, GCS, and Azure object-storage backends, the Node.js and browser embedded engines are rebuilt against the current engine, and a deep round of index work lands: bitmap index fusion, pre-filtered vector search, and a much faster full-text write path. For SurrealDS (Enterprise), cold-start convergence is dramatically faster, recovery cost is bounded, and cluster storage can now be S3-backed.

The items below cover what is new in the 3.3 line. The fixes already released across the v3.2.1v3.2.4 patches are also included in this build but are not repeated here.

Items tagged (Enterprise) on a heading or [Enterprise] on a bullet refer to the SurrealDB Enterprise product. Everything else applies to community.

SurrealDB can now speak the Postgres wire protocol (v3.0), so any Postgres client - psql, JDBC, npgsql, tokio-postgres, and the rest - can connect and run SurrealQL or ISO GQL with properly typed results.

  • Opt-in via --postgres-bind <addr> / SURREAL_POSTGRES_BIND, and capability-gated through a new postgres route target. TLS is available through the standard SSLRequest upgrade, reusing --web-crt / --web-key.

  • Both the simple and extended query protocols are supported (Parse/Bind/Describe/Execute/Close/Sync/Flush), with text and binary codecs for every mapped type including numeric. The namespace and database come from the startup database parameter as ns/db, with USE and LET persisting across queries; interactive BEGIN/COMMIT/ROLLBACK behaves like Postgres, including 25P02 aborted-transaction poisoning, and CancelRequest is supported.

  • The query dialect is selectable at connect time (options=-c dialect=gql) or in-session (SET dialect), so a Postgres client can run ISO GQL directly.

  • Authentication supports cleartext passwords over TLS and SCRAM-SHA-256 (RFC 5802 / RFC 7677). DEFINE USER ... PASSWORD now derives SCRAM verifier material alongside the existing Argon2 hash, and a new additive PASSSCRAM '<verifier>' clause on DEFINE USER / ALTER USER imports a precomputed PostgreSQL-format verifier so exports round-trip losslessly. Existing users without SCRAM material keep working.

  • Postgres positional parameters $1..$n are rewritten to $_1..$_n, and SurrealDB auto-commits each top-level statement - use an explicit BEGIN ... COMMIT for all-or-nothing behaviour. ANSI-SQL translation, pg_catalog emulation, MD5/COPY, and LIVE queries are out of scope for now.

ISO GQL querying - introduced experimentally in 3.2 - is now available by default, matching GraphQL. The --allow-experimental gql flag, the SDK experimental-feature builder, and the gql cargo feature all remain valid as harmless no-ops, so existing configurations keep working. GQL is reachable over the /gql HTTP route, the gql RPC method, MCP, and the new Postgres listener.

The SDK's engines now sit behind one typed SurrealEngine interface, and a new native gRPC engine joins the family alongside major streaming work across every transport.

  • New grpc:// / grpcs:// connection schemes, shipped enabled in the released surreal binary, so surreal sql --endpoint grpc://host:port works out of the box.

  • Query results stream end to end over gRPC via db.query(..).stream_items() - the client no longer waits for the whole result set to be produced. Measured on a 10,000-row SELECT, the first rows reach the client 65 µs after execution begins, against ~20 ms to produce them all. Rows are provisional until their statement's terminal frame arrives, which is withheld until the outcome is final.

  • The WebSocket RPC protocol gains a query_stream request answered by a sequence of frames (begin, per-statement rows/value/finished, then one end), carried in JSON, CBOR, and flatbuffers alike. This brings streaming to the places gRPC cannot reach: browsers/WASM and Cloudflare Workers.

  • The embedded JavaScript engines stream now too: a QueryStream with next() in @surrealdb/node-native and a ReadableStream in @surrealdb/wasm-native, with bounded buffering so nothing runs ahead of the reader, and abandoned streams stopping their execution instead of orphaning a transaction.

File buckets (DEFINE BUCKET and the file::* functions) gain cloud object-storage backends in the open-source engine, built on the object_store crate.

  • AWS S3 and S3-compatible stores (MinIO, Backblaze B2, Wasabi, Cloudflare R2) via s3://, s3+http://, s3+https://; Google Cloud Storage via gs:// / gcs://; Azure Blob Storage via az:// / azure://. The existing S3 URL format is preserved exactly, and a latent bug that dropped a custom endpoint's port is fixed.

  • Bucket traffic is now metered: three authenticated-only OpenTelemetry instruments under surrealdb.bucket - sent_bytes, received_bytes, and operations.

  • Embedders can install a custom object store behind buckets with Builder::with_bucket_store_provider(...) - the seam needed to back buckets with a platform store such as a Cloudflare Workers R2 binding.

The streaming planner can now compose index results as bitmaps over a shared per-table document-ID space, and vector search can use those bitmaps to filter before traversal.

  • Index-backed predicates compose with AND/OR/NOT as compressed bitmaps, and a COUNT answerable from indexes alone never touches records. New plan nodes (BitmapIndexScan, BitmapFullTextScan, BitmapAnd, BitmapOr, BitmapAndNot) are visible in EXPLAIN, with per-node candidate cardinalities in EXPLAIN ANALYZE. Existing b-tree indexes keep working with no rebuild. Non-anchor range branches are capped by SURREAL_BITMAP_BRANCH_BUDGET (default 250,000 entries; 0 disables), and the full WHERE clause is always retained as a residual filter.

  • Pre-filtered vector search. When a KNN plan is chosen, WHERE conjuncts that provably match an index bitmap are evaluated into an allow-list before HNSW/DiskANN traversal, replacing one record fetch per visited graph node. An adaptive triage picks a tier, reported in EXPLAIN ANALYZE as prefilter_tier: exact (small allow-lists skip the graph entirely for guaranteed-correct top-K, threshold SURREAL_KNN_PREFILTER_EXACT_THRESHOLD, default 2,000), graph (gated traversal with a boosted search width, up to SURREAL_KNN_PREFILTER_EF_BOOST_THRESHOLD, default 100,000), and graph_unboosted above that, with a fallback tier reported when the allow-list cannot be built within the bitmap branch budget. Enabled by default; SURREAL_KNN_PREFILTER_ENABLED=false turns it off.

The write path of the concurrent full-text index has been overhauled end to end.

  • Full-text delta-log entries are batched per transaction instead of one key per (term, document), and postings are now keyed by document rather than by term which significantly reduces write amplification. Older indexes are read through a legacy fallback, so no migration is needed.

  • COUNT-index deltas are aggregated per transaction, and the compactor is woken by the commit that queues its work rather than only by its timer.

  • Exports now size each emitted INSERT statement by the KV keys a record will write given the table's index set, so restores of heavily indexed tables use appropriately sized transactions.

A major round of work on SurrealDS cluster formation, recovery cost, and overload behaviour.

  • Faster cold-start convergence. Replicas leaving recovery at different times could stall cluster formation on retry timers. View-change retransmission, a formation-aware backoff, and concurrent outcome-donor drains cut measured convergence tails from 4.44 s to 0.02 s (n=3) and 14.75 s to 4.61 s (n=5), and the default-config worst case from 86.9 s to 13.7 s.

  • Bounded recovery drains. The Phase-1 outcome drain previously re-paged a donor's entire outcome history on every entry into recovery - O(all transactions ever committed). Each replica now keeps a bounded in-memory write-order journal and a recovering replica asks each donor only for what it wrote since its last position, falling back to the full stream when needed. Configured via SURREAL_DS_RECOVERY_OUTCOME_JOURNAL_ENTRIES (default 262,144; 0 disables). Drain behaviour is observable on new surrealdb.ds.recovery_outcome_drain* counters.

  • Bounded transaction write sets. A new operation-count bound on transaction write sets, SURREAL_DS_TRANSACTION_WRITE_SET_LIMIT_OPS (default 100,000; 0 disables), sits beside the existing 32 MiB byte cap: an over-bound transaction fails fast at the coordinator with TransactionTooManyOperations before any network traffic, with a warning fired once at half the bound. Keep the value uniform across the fleet.

  • Membership-epoch correctness. A membership-changing leader now withholds serving until a quorum of the new voter set is confirmed to hold the decided configuration; a coordinator whose epoch was superseded mid-transaction abandons the round promptly instead of resending into a silent fence; and a voter fenced for being ahead of its own cluster now escalates to a view change instead of starving indefinitely. The fence itself is now observable via the surrealdb.ds.epoch_fence_drops counter with a direction label.

  • Mutable permission clauses, behind a capability. The 3.1-era security block on writes in all permission clauses over-reached for create/update/delete clauses, where audit-logging side effects are a relied-upon pattern. Those are re-permitted behind a new transitional mutable_permissions experimental capability - allowed by default on the server (deny it with --deny-experimental mutable_permissions), off by default for embedders. SELECT permission clauses stay read-only. Usage is measurable via the new surrealdb_statement_mutable_permission_writes_total counter. Alongside this, function mutability is now resolved against the stored call graph, so a write hidden behind a user-defined function call can no longer slip past definition-time checks on COMPUTED fields and permission clauses.

  • Datastore versioning and startup migrations. The datastore now records its semantic version and runs registered, ledger-tracked data migrations on startup, resuming interrupted runs, with a per-node version history. The first migration fixes a keyspace collision present since 3.0.0, where DEFINE SEQUENCE keys sorted inside the table-name band and any table whose name began with sq could break INFO FOR DB, sequence exports, and REMOVE NAMESPACE / REMOVE DATABASE. Note for rolling upgrades: a sequence created after the rollout begins is not visible to nodes still on the previous release until they upgrade.

  • Wall-clock query timeouts on the transports. The HTTP (/sql, /gql, /graphql) and RPC (WebSocket and HTTP) surfaces now apply a hard wall-clock timeout reusing the configured --query-timeout value, on top of the existing cooperative deadline. Timed-out HTTP requests return 504 Gateway Timeout with a proper error envelope. Off unless --query-timeout is set; transaction-control methods are exempt, while signin/signup/authenticate (which can run user-defined SurrealQL) are guarded.

  • Runtime-swappable capabilities for embedders. Datastore::set_capabilities(...) atomically swaps the capability set with no datastore rebuild, aimed at embedders that cannot restart per instance (such as a wasm Cloudflare Durable Object serving many tenants). In-flight queries keep the snapshot they started with, and the outbound HTTP client is rebuilt in the same swap so a runtime network tightening is enforced on redirect hops and DNS resolution too.

  • Stateless MCP. The first-party MCP server now serves the stateless 2026-07-28 protocol revision alongside the handshake-based revisions on the same /mcp endpoint. Every tool except use gains optional namespace and database arguments, with scope resolving from call arguments, then surreal-ns / surreal-db request headers, then session state, then server defaults. The advertised protocol revision is now pinned explicitly (upgrading the underlying SDK can no longer silently change it), and the gql tool's annotations no longer declare it read-only - those hints drive client-side auto-approval, and the GQL dialect can write.

  • vector::sum. A new function, in both forms: as a grouped aggregate it folds a vector-valued expression across a group's rows in O(dimension) state regardless of row count, and as a scalar it sums an array<array<number>> directly - making an in-database weighted centroid composable from existing functions.

  • New vector functions, stricter numeric edge cases. vector::distance::mahalanobis(a, b, cov) and vector::similarity::spearman(a, b) are implemented. A family of silent-NaN or sentinel results now error instead: vector::divide with a zero divisor element, normalize/angle/project at zero magnitude, and pearson/spearman on degenerate inputs; vector::similarity::jaccard now uses true set semantics, and time::nano errors outside the i64-nanosecond range instead of returning 0.

  • ORDER BY outside the projection. SELECT event, subject FROM audit_log ORDER BY at now works - the sort field no longer has to appear in the projection.

  • Brute-force KNN with computed query vectors. The streaming executor now supports <|k, DIST|> with a non-literal query vector (bind parameters, function calls, computed arrays), closing the last unimplemented case in the read-only planner.

  • Multi-part MATCHES. A full-text @@ reached through a record link to an index on another table (t.name @@ 'x') now executes on the streaming engine.

  • Index matching through record traversals. A WHERE clause matching a compound index through record-idiom traversals of a parameter (for example WHERE createdAt <= $scan.task.finishedAt) previously always full-table-scanned; row-independent traversals are now resolved at plan time so index analysis can match them, with strict provable-equivalence conditions.

  • Silo package resolution. Surrealism silo packages now resolve over HTTPS from a configurable endpoint (default https://silo.surrealdb.com), with a 256 MiB cap and organisation- and package-name restrictions that block path traversal. The fetch needs no allow_net grant since the host comes from server configuration.

  • Import and scan performance. Collection literals whose elements are already values now convert without the async evaluator (a 200K-record import profile had 52.9% of CPU under literal evaluation); mock targets (|table:N|) are drained id-by-id instead of pre-expanding a million-entry list; nine scan loops stopped copying whole cursor batches; and a delete's reference batch is decoded once instead of per entry.

  • Quieter, correct system metrics. One system-metrics refresher now runs per process regardless of datastore count (embedders with several datastores got CPU figures from arbitrary sub-intervals), and the metrics cache is populated before the first query so INFO FOR ROOT can no longer report an all-zero system block.

  • Storage engine updates. The memory backend adopts surrealmx 0.24's native savepoint release, removing an emulated savepoint stack; affinitypool 0.8 speeds up the blocking-work pools and fixes a pool deadlock; TiKV gRPC status failures are now logged with their code and message at debug level instead of being collapsed.

  • [Enterprise] Recovery and starvation observability. The Phase-1 outcome drain's certification behaviour is surfaced on three new counters; the starved-begin metric is split by starvation class (below_quorum, no_local_reply - dashboards summing only anterior_prepared will now under-report); and the log volume of a membership reconfiguration is bounded and rate-paced.

  • [Server] USE NS <expr> / USE DB <expr> with a subquery argument hit an internal panic; such statements now return an ordinary query error with the transaction rolled back.

  • [Query] A statement's read-only classification had three holes (SELECT clauses like LIMIT (CREATE ...), RETURN ... FETCH, and several idiom parts), so writing statements could run on read transactions and fail partway through.

  • [Query] rand::* calls in a WHERE clause were folded into a single plan-time constant on the streaming engine, making the predicate all-or-nothing across rows; they now evaluate per row, as SQL engines do for volatile functions.

  • [Query] Numeric hash/equality mismatches on the streaming engine: GROUP BY could split one value into two groups, and IN / INSIDE / CONTAINSANY / CONTAINSALL could silently drop rows when a column mixed float and decimal representations of the same number. Grouping and set-membership now key on ordering, matching =.

  • [Query] CONTAINSALL with mixed-representation literals dropped rows on the index-overlap path; literal equivalence classes now each get exactly one bitmap bit. Separately, a UnionIndexScan could silently drop a CONTAINSANY / ANYINSIDE conjunct from its residual filter and return rows that did not match the original WHERE clause.

  • [Query] RETURN DISTINCT emitted one value as two rows when float and decimal forms of the same number hashed differently; dedup and hash-joins now key on ordering, which also halves peak join build memory.

  • [Query] [WHERE ...] applied to a non-collection returned a one-element array instead of NONE, and sets were not filtered element-wise. Separately, ORDER BY ties are now stable (input order as the final key), so a LIMIT cutting inside a tie group can no longer repeat or drop rows across pages.

  • [Query] The streaming executor's MATCHES fallback returned true unconditionally (WHERE 'abc' @@ 'zzz' matched every row); and the legacy engine's cross-table MATCHES evaluated a tautology that matched every row. Roughly 490 unit tests were backfilled across the executor at the same time.

  • [Query] BREAK / CONTINUE raised inside a LET binding's value were swallowed by the streaming executor instead of propagating to the enclosing loop.

  • [Query] PERMISSIONS predicates reaching the row via $parent denied every row on the streaming engine, and math::variance / math::stddev returned population figures where sample figures were correct, in both ad-hoc GROUP BY and materialized views. The field-permission pass could also cut whole array elements out of results, causing silent data loss including an empty RETURN DIFF for non-owner sessions.

  • [Query] A closure body's writes are now reflected in its access mode, so array::map with a writing closure can no longer be scheduled on read-only execution paths; likewise an unresolvable mod:: function's write flag now defaults to writeable.

  • [Query] 85 signature divergences between the streaming function registry and the legacy layer were aligned; six api::* middleware functions were missing from the streaming registry entirely, and several async builtins silently ignored extra arguments instead of erroring. object::matches - advertised by the parser but implemented in neither registry, so every call failed at runtime - is now rejected cleanly at parse time.

  • [Query] Values now coerce into set types, so SET field += value on a set<...> field works, and deduplication is applied after a field's VALUE clause runs rather than before.

  • [Index] Any index rebuild could publish a durably Online but empty index (the compactor's generation guard was wiped with the build subspace), after which every query against it silently returned nothing. Covers DEFINE INDEX, DEFINE INDEX OVERWRITE, and REBUILD INDEX.

  • [Index] Rolling back a DEFINE INDEX ... CONCURRENTLY could leave durable build state behind, because cleanup deleted the build keys while the builder task was still writing.

  • [Index] A full-text term's compacted document set could depend on how compaction rounds grouped its deltas, so a document could keep a term it had lost (a false MATCHES hit) with nothing to correct it later; compaction now carries a per-term residual. Full-text compaction rounds are also now bounded by a document budget rather than a key count.

  • [Index] DiskANN KNN memory and latency grew without bound under sustained write load (OOM-cycling on capped pods); the pending-update set is now sharded so per-query work tracks the active backlog.

  • [RPC] A single client could permanently deadlock its own session across 15 RPC methods over WebSocket, HTTP, or gRPC, when a buffered query overlapped a session-mutating call (set, signin, use, ...).

  • [RPC] Three paths dropped writeable transactions without committing or cancelling them - worst was async event processing, where an undecodable queue entry leaked a transaction on every batch indefinitely. Present since 3.1.

  • [RPC] Live-query registry leaks: an ended live query kept its registry entry and its surrealdb.live_query.active gauge increment for the life of the connection; REMOVE TABLE/DATABASE/NAMESPACE and principal revocation are now covered, and gRPC streams delete subscriptions the client never claimed.

  • [RPC] Dropping a client-side item stream mid-query leaked its transaction and driving tasks for the life of the process; the execution now stops when the stream is dropped. The per-connection WebSocket stream cap is also now claimed atomically.

  • [SDK] Cloning a Surreal handle on the WebSocket engine replayed session state fire-and-forget, so the clone's first request could execute before its own signin - reproduced at roughly 1% of iterations. Requests now park until the replayed setup is acknowledged.

  • [Embedded] db.use_defaults() cleared the session instead of applying DEFINE CONFIG DEFAULT, so embedded consumers silently ignored configured default namespaces/databases.

  • [Embedded] A query abandoned mid-batch left a half-written transaction in the session that a later commit would commit; abandoned queries now take their transaction with them. The session registry no longer auto-creates entries for already-ended sessions.

  • [Export] A database with an ENFORCED relation table could not restore its own export - every edge was silently dropped while the restore reported success. The endpoint check is now deferred under OPTION IMPORT, fixing existing export files too.

  • [HTTP] /signin and /signup each applied the other's body-size limit; each route now uses its own. On wasm, http::* calls failed capability checks outright and carried no User-Agent; both fixed. fetch() with redirect: "error" / "manual" could not fetch any URL because its client was built from the wrong rule set.

  • [KV] Releasing a savepoint discarded the wrong scope's undo entries on every backend, so a record created by a statement that later failed could survive along with its index writes (reachable via synchronous DEFINE EVENT). Also fixed a TiKV regression that roughly doubled write-path RPCs for multi-record INSERT/UPSERT after any savepoint release.

  • [KV] surrealkv is updated to 0.21.3, carrying two durability fixes - a missing fsync after compaction that could leave a SIGKILL'd pod unable to start, and a memtable rotation bug.

  • [KV] The DEFINE API handler's transaction now applies the statement write-cardinality guard (SURREAL_TRANSACTION_MAX_WRITE_KEYS), which previously did not cover custom API handlers.

  • [Parser] More legacy SurrealQL accepted by the new parser (SET idiom forms, function-call sleep, keyword record ids in RELATE targets), and RecordId::parse_simple no longer adds a layer of backticks on every round trip.

  • [Enterprise] Recovery could abort a committed, durably applied transaction when a replica's last transaction-id-keyed evidence was reaped exactly as its applied gate was set; votes are now retained until a real id-keyed decision exists.

  • [Enterprise] OCC read sets are now validated against the writes that can actually invalidate them, tightening isolation correctness.

  • [Enterprise] An indeterminate commit is now reported as CommitOutcomeUnknown - telling the client to read back rather than replay - instead of a definite "nothing was written" error.

  • [Enterprise] Both Transactable implementations discarded a released savepoint's undo state, mirroring the community savepoint fix.

The gql experimental capability gate is removed and ISO GQL querying is available by default. Deployments that relied on it being off should control access via the gql route target and the standard capability configuration. The old --allow-experimental gql spelling remains accepted as a no-op.

UPDATE and UPSERT now evaluate the WHERE condition before the data clause, so a side-effecting data clause (for example SET spawn = (CREATE log).id) no longer runs for records the condition rejects - previously the side-effect count could depend on whether the planner picked an index. The condition and data clause now share a single pre-mutation snapshot, so reads never observe the statement's own writes, and field clauses evaluate in dependency order rather than alphabetically. Review statements that relied on assignment-visibility or alphabetical field ordering.

Three syntax changes to the experimental Surrealism module surface: the executable form now uses FROM instead of AS (DEFINE MODULE mod::color FROM f"modules:/color.surli" UNSIGNED;), a trailing UNSIGNED keyword is now required on both executable forms, and the silo version segment is now written silo::{org}::{pkg}::<1.0.0> (with :: before the version). Stored definitions are unaffected and re-render in the new syntax.

The memory backend's versioned-read support is removed: VERSION clauses return an unsupported-versioned-queries error, matching indxdb, and the backend now rejects versioned / retention connection parameters at startup instead of accepting them and failing later. Version coverage lives in the rocksdb and surrealkv backends, which retain native versioning.

All seven duration::set_* functions were parser-advertised but never implemented - every call failed at runtime. They now produce a clean parse-time error.

A COMPUTED field or permission clause whose body writes through a user-defined function call was previously accepted (and wrote on every read); function mutability is now resolved against the stored call graph and such definitions are refused at DEFINE/ALTER time.

The SDK's engine layer is now the typed SurrealEngine trait (one method per operation) rather than a command enum over a channel; SDK futures remain Send but are no longer Sync. surrealdb-core no longer re-exports surrealdb-rpc, so Rust consumers must depend on it directly. Datastore::transaction(..) loses its ignored lock-type argument, and Datastore::get_capabilities() returns an owned Arc<Capabilities>.

@surrealdb/node is split: the native addon is published as @surrealdb/node-native and the SDK engine implementation moves to surrealdb.js. The strict connection option - advertised in the typings but silently ignored - is removed and is now a type error. The wasm module is renamed surrealdb_wasm and published as @surrealdb/wasm-native.

Newer patch available

Upgrade to 3.3.0-beta.4

You are viewing the 3.3.0-beta.1 changelog. A newer patch in this release line is available - we recommend running 3.3.0-beta.4 for the latest fixes and improvements.

View 3.3.0-beta.4 release notes

Our newsletter

Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks

GET STARTED

Start building with SurrealDB

The unified data layer for AI. Simplify your stack. Reduce complexity. Build faster.

SamsungNVIDIAAppleVerizonTencent

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

SurrealDB

The unified data layer for AI

Graph, vector, document, and relational in one engine.
Agent Memory that connects and retrieves context wherever your data lives.

Explore with AI

Stay in the loop

Tutorials, AI agent recipes, and product updates, every two weeks.

Independently verified

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

Trust Centre

Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201

Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom

Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom