3.3.0-beta.4
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.
Highlights
🕸 Faster traversals, and edges that carry their own data
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 ... LIMITtraversals stop early at the scan. The gain is largest over high-degree vertices and from many starting records, andcount(->edge)degree counts are served straight from adjacency storage instead of materialising every edge.LIGHTWEIGHTrelations drop the storage cost of a pure adjacency edge.DEFINE TABLE follows TYPE RELATION IN person OUT person LIGHTWEIGHTcreates record-less edges storing nothing beyondid,inandout.RELATEis idempotent and returns an edge synthesised on the fly, with the canonical idfollows:[person:a, person:b]; traversals, scans,count(), ordering, point reads and projections synthesise it likewise, and deleting a vertex cascades to its edges.IN/OUTare required andENFORCEDis implied. Such a relation cannot carry fields, indexes or events, take a data clause or a custom edge id, beUPDATEd orINSERTed into, or beSCHEMAFULL,CHANGEFEEDorDROP. They suit pure adjacency (follows, likes) where the edge needs no properties.INLINEfields answer a filtered traversal without reading edge records.DEFINE FIELD score ON likes TYPE number INLINEembeds the field value into the edge's adjacency entry, withALTER FIELD ... INLINE/... DROP INLINEto 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-COMPUTEDfields on an existingTYPE RELATIONtable qualify (notid/in/out, and not onLIGHTWEIGHTrelations). 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 TABLEandALTER TABLEacceptINLINE EDGES <n>andINLINE REFERENCES <n>caps, 0 to 256, removed withDROP 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.
🪪 Row permissions that resolve once per session, and stricter JWT checks
CONTEXTcomputes a permission's payload once per session instead of once per row.DEFINE ACCESS ... TYPE RECORDaccepts an optionalCONTEXT ( <expr> ), evaluated once at authentication time with$authbound 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 byDURATION FOR SESSION, and is recomputed on re-authentication;$authis 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 CONTEXTremoves it.AUDIENCErejects a token minted for a different service.DEFINE ACCESS ... TYPE JWT, and record access configuredWITH JWT, accepts an optionalAUDIENCE "aud1", "aud2"clause. When present, a presented token'saudclaim - 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 ignoreaud, so existing definitions are unaffected. It works at root, namespace and database level, and the configured audiences are surfaced byINFO.
🔒 Read-modify-write without losing an update
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.
🔑 Pull SurrealDB Enterprise straight from Docker Hub (Enterprise)
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.
Improvements
[Query]Disjunctive and reachability filters now run at index speed. Top-levelORfilters across indexes fuse into a single bitmap union, and graph-reachability sub-conditions inside aWHEREcompile 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'smethodfield when no built-in method of that name applies; built-ins such aslenstill 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 aWARNnaming 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. AREBUILD INDEXissued 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, soSURREAL_MEMORY_THRESHOLDmeasures 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/healthresponds straight away, with the remaining work behind a readiness gate during which requests receive 503 and/readyreports 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_SIZEbounds 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/rpcbody 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 matchingGrpcConfig(surrealdb::opt::GrpcConfig::max_message_size, viaConfig::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 APIgains anapi::req::max_body(limit)middleware that answers HTTP 413 on a raw body exceedinglimit, given as a non-negative byte count or a size string such as"1mb"or"512kb". Only rawbytesandstringbodies are measured; place it ahead ofapi::req::body(...)so oversized payloads are never parsed.
Bug fixes
[Query]An ordinarySELECTused as an inline subquery value - most visibly as aFORrange,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 toNONE.[Query]Parentheses around an equal-precedence sub-expression are now preserved when an expression is serialised back to SurrealQL text, so aDEFINE FUNCTIONorDEFINE FIELDbody computes the same result after being reloaded. PreviouslyRETURN $x / ($a * $b)was re-emitted without them and reparsed as($x / $a) * $b.[Query]SELECT count()now agrees withSELECTon 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 return0whileSELECTreturned them.[Query]LIMIT/STARTare now kept above the sort when anORDER BYis withheld from access-path selection because its sort field carries a field-levelSELECTpermission. 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 examplefunctionorselect) now round-trips through the schema catalog, surviving laterDEFINE FIELDstatements 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, sosilo::acme::widgets<1.2.3>written by an earlier release no longer matched thesilo::acme::widgets::<1.2.3>the current build derives:REMOVE MODULEandALTER MODULEreported it missing, calls to its exports could not reach it, andOVERWRITEwrote a second row, all whileINFO FOR DBlisted it correctly. Upgrading moves every mismatched row to the key its own value derives, carrying permissions, comments and theunsignedflag across. Onlysilo::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 bySURREAL_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 aResetWithoutClosingHandshaketransport error.[Scripting]Reading response headers in a scriptingfetchno longer aborts the process when a remote server returns a header value containing non-ASCII or non-UTF-8 bytes. TheHeadersaccessors (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 aCONCURRENTLYbuild 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.
Upgrade notes
[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 runREBUILD 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_KEYorSURREAL_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.
- Open the SurrealDB Studio
- Select your organisation and instance
- On the dashboard, click on the "Upgrade" button
- Your instance will be updated and restarted automatically
3.3.0-beta.3
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.
Improvements
[Query]Permission andCOMPUTEDwrite rules move to runtime enforcement.v3.3.0-beta.1added a definition-time check that tried to decide, when a schema object was defined, whether aPERMISSIONSguard orCOMPUTEDbody 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 callsCREATE/UPDATEwas refused for every caller, including the guards andCOMPUTEDfields that never take that branch. That check is dropped: aPERMISSIONSclause loses its definition-time write refusal entirely, and aCOMPUTEDbody loses it for writes reached through a function call. A write written directly in aCOMPUTEDbody is still refused atDEFINE/ALTERtime. ADEFINE/ALTERthat stores a writingSELECTguard, or aCOMPUTEDbody that reaches a write through a function call, is now accepted, and fails at the point a read actually reaches the write, with the existingA 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/deletepermission 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 inv3.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 ownos/cpu/libc, and the root package lists them all underoptionalDependencies, so an install downloads only the one binary its host can use rather than all eight, keeping a@surrealdb/nodeinstall under AWS Lambda's 250 MB unzipped limit.
Bug fixes
[Embedded]free()on@surrealdb/node-nativenow releases the datastore's storage before it resolves, so a file-backed datastore can be reopened on the same path afterclose()within one process. surrealkv releases its lockfile late in shutdown andDroponly spawned a detached close, sofree()previously returned with the data directory still locked. The wasm engine has no file backends and is unaffected.[Embedded]Thesurrealkv+versioned://scheme, which has named no backend since versioning moved to the?versioned=trueparameter, 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-streambeginframes now carry the protocol revision, and the structural contract a client can rely on when decoding is written down: astreamtag is never repurposed, unknown tags and fields are ignorable, andsingleis always present on afinishedframe (it was already on the wire; only the SDK type made it optional).
Upgrade notes
The
mutable_permissionsexperimental capability is removed. It existed only to reopen thecreate/update/deleteguards that the definition-time rule had shut, and was on for every server already. Remove any--allow-experimental mutable_permissions/--deny-experimental mutable_permissionsflag and the SDK'sExperimentalFeature::MutablePermissionsbuilder entry; there is no replacement, since those guards now permit side effects unconditionally.The
surrealdb_statement_mutable_permission_writes_totalmetric 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.
3.3.0-beta.2
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.
Improvements
[Server]Configurable readiness heartbeat window. The/readyprobe 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 flipNotReadyat 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, soshutdownwaits 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.
Bug fixes
[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, andremove_nodesopened 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 durablemetacolumn 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 aNormal-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 retryableTransactionConflictrather 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.
Upgrade notes
[Enterprise]If you appliedSURREAL_DS_STARTUP_NORMAL_TIMEOUT=0as a break-glass onv3.3.0-beta.1, unset it on this build. The startup warm-up now retries through view-transition churn, so=0only removes protection - it disables both enterprise startup gates and leavescheck_versionas the only one.[Enterprise]KeepSURREAL_STARTUP_OPERATION_TIMEOUT=180for 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.
3.3.0-beta.1
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.1–v3.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.
Highlights
🐘 Postgres wire protocol
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 newpostgresroute target. TLS is available through the standardSSLRequestupgrade, 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 startupdatabaseparameter asns/db, withUSEandLETpersisting across queries; interactiveBEGIN/COMMIT/ROLLBACKbehaves like Postgres, including25P02aborted-transaction poisoning, andCancelRequestis 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 ... PASSWORDnow derives SCRAM verifier material alongside the existing Argon2 hash, and a new additivePASSSCRAM '<verifier>'clause onDEFINE USER/ALTER USERimports a precomputed PostgreSQL-format verifier so exports round-trip losslessly. Existing users without SCRAM material keep working.Postgres positional parameters
$1..$nare rewritten to$_1..$_n, and SurrealDB auto-commits each top-level statement - use an explicitBEGIN ... COMMITfor all-or-nothing behaviour. ANSI-SQL translation,pg_catalogemulation, MD5/COPY, andLIVEqueries are out of scope for now.
🕸 ISO GQL enabled by default
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.
📡 A gRPC engine and end-to-end result streaming
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 releasedsurrealbinary, sosurreal sql --endpoint grpc://host:portworks 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-rowSELECT, 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_streamrequest answered by a sequence of frames (begin, per-statementrows/value/finished, then oneend), 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
QueryStreamwithnext()in@surrealdb/node-nativeand aReadableStreamin@surrealdb/wasm-native, with bounded buffering so nothing runs ahead of the reader, and abandoned streams stopping their execution instead of orphaning a transaction.
🪣 Cloud object storage for buckets
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 viags:///gcs://; Azure Blob Storage viaaz:///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, andoperations.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.
🎯 Bitmap index fusion and pre-filtered vector search
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
COUNTanswerable from indexes alone never touches records. New plan nodes (BitmapIndexScan,BitmapFullTextScan,BitmapAnd,BitmapOr,BitmapAndNot) are visible inEXPLAIN, with per-node candidate cardinalities inEXPLAIN ANALYZE. Existing b-tree indexes keep working with no rebuild. Non-anchor range branches are capped bySURREAL_BITMAP_BRANCH_BUDGET(default 250,000 entries;0disables), and the fullWHEREclause is always retained as a residual filter.Pre-filtered vector search. When a KNN plan is chosen,
WHEREconjuncts 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 inEXPLAIN ANALYZEasprefilter_tier:exact(small allow-lists skip the graph entirely for guaranteed-correct top-K, thresholdSURREAL_KNN_PREFILTER_EXACT_THRESHOLD, default 2,000),graph(gated traversal with a boosted search width, up toSURREAL_KNN_PREFILTER_EF_BOOST_THRESHOLD, default 100,000), andgraph_unboostedabove that, with afallbacktier reported when the allow-list cannot be built within the bitmap branch budget. Enabled by default;SURREAL_KNN_PREFILTER_ENABLED=falseturns it off.
⚡ Faster full-text and count indexes
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
INSERTstatement by the KV keys a record will write given the table's index set, so restores of heavily indexed tables use appropriately sized transactions.
❄️ SurrealDS cold starts, recovery, and admission (Enterprise)
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;0disables). Drain behaviour is observable on newsurrealdb.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;0disables), sits beside the existing 32 MiB byte cap: an over-bound transaction fails fast at the coordinator withTransactionTooManyOperationsbefore 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_dropscounter with adirectionlabel.
Improvements
Mutable permission clauses, behind a capability. The 3.1-era security block on writes in all permission clauses over-reached for
create/update/deleteclauses, where audit-logging side effects are a relied-upon pattern. Those are re-permitted behind a new transitionalmutable_permissionsexperimental capability - allowed by default on the server (deny it with--deny-experimental mutable_permissions), off by default for embedders.SELECTpermission clauses stay read-only. Usage is measurable via the newsurrealdb_statement_mutable_permission_writes_totalcounter. 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 onCOMPUTEDfields 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 SEQUENCEkeys sorted inside the table-name band and any table whose name began withsqcould breakINFO FOR DB, sequence exports, andREMOVE 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-timeoutvalue, on top of the existing cooperative deadline. Timed-out HTTP requests return504 Gateway Timeoutwith a proper error envelope. Off unless--query-timeoutis set; transaction-control methods are exempt, whilesignin/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-28protocol revision alongside the handshake-based revisions on the same/mcpendpoint. Every tool exceptusegains optionalnamespaceanddatabasearguments, with scope resolving from call arguments, thensurreal-ns/surreal-dbrequest 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 thegqltool'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 anarray<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)andvector::similarity::spearman(a, b)are implemented. A family of silent-NaN or sentinel results now error instead:vector::dividewith a zero divisor element,normalize/angle/projectat zero magnitude, andpearson/spearmanon degenerate inputs;vector::similarity::jaccardnow uses true set semantics, andtime::nanoerrors outside the i64-nanosecond range instead of returning 0.ORDER BYoutside the projection.SELECT event, subject FROM audit_log ORDER BY atnow 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
WHEREclause matching a compound index through record-idiom traversals of a parameter (for exampleWHERE 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 noallow_netgrant 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 ROOTcan no longer report an all-zerosystemblock.Storage engine updates. The memory backend adopts surrealmx 0.24's native savepoint release, removing an emulated savepoint stack;
affinitypool0.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 onlyanterior_preparedwill now under-report); and the log volume of a membership reconfiguration is bounded and rate-paced.
Bug fixes
[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 (SELECTclauses likeLIMIT (CREATE ...),RETURN ... FETCH, and several idiom parts), so writing statements could run on read transactions and fail partway through.[Query]rand::*calls in aWHEREclause 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 BYcould split one value into two groups, andIN/INSIDE/CONTAINSANY/CONTAINSALLcould silently drop rows when a column mixedfloatanddecimalrepresentations of the same number. Grouping and set-membership now key on ordering, matching=.[Query]CONTAINSALLwith mixed-representation literals dropped rows on the index-overlap path; literal equivalence classes now each get exactly one bitmap bit. Separately, aUnionIndexScancould silently drop aCONTAINSANY/ANYINSIDEconjunct from its residual filter and return rows that did not match the originalWHEREclause.[Query]RETURN DISTINCTemitted one value as two rows whenfloatanddecimalforms 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 ofNONE, and sets were not filtered element-wise. Separately,ORDER BYties are now stable (input order as the final key), so aLIMITcutting inside a tie group can no longer repeat or drop rows across pages.[Query]The streaming executor'sMATCHESfallback returnedtrueunconditionally (WHERE 'abc' @@ 'zzz'matched every row); and the legacy engine's cross-tableMATCHESevaluated a tautology that matched every row. Roughly 490 unit tests were backfilled across the executor at the same time.[Query]BREAK/CONTINUEraised inside aLETbinding's value were swallowed by the streaming executor instead of propagating to the enclosing loop.[Query]PERMISSIONSpredicates reaching the row via$parentdenied every row on the streaming engine, andmath::variance/math::stddevreturned population figures where sample figures were correct, in both ad-hocGROUP BYand materialized views. The field-permission pass could also cut whole array elements out of results, causing silent data loss including an emptyRETURN DIFFfor non-owner sessions.[Query]A closure body's writes are now reflected in its access mode, soarray::mapwith a writing closure can no longer be scheduled on read-only execution paths; likewise an unresolvablemod::function's write flag now defaults to writeable.[Query]85 signature divergences between the streaming function registry and the legacy layer were aligned; sixapi::*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 intosettypes, soSET field += valueon aset<...>field works, and deduplication is applied after a field'sVALUEclause runs rather than before.[Index]Any index rebuild could publish a durablyOnlinebut empty index (the compactor's generation guard was wiped with the build subspace), after which every query against it silently returned nothing. CoversDEFINE INDEX,DEFINE INDEX OVERWRITE, andREBUILD INDEX.[Index]Rolling back aDEFINE INDEX ... CONCURRENTLYcould 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 falseMATCHEShit) 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 itssurrealdb.live_query.activegauge increment for the life of the connection;REMOVE TABLE/DATABASE/NAMESPACEand 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 aSurrealhandle on the WebSocket engine replayed session state fire-and-forget, so the clone's first request could execute before its ownsignin- reproduced at roughly 1% of iterations. Requests now park until the replayed setup is acknowledged.[Embedded]db.use_defaults()cleared the session instead of applyingDEFINE 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 latercommitwould 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 anENFORCEDrelation table could not restore its own export - every edge was silently dropped while the restore reported success. The endpoint check is now deferred underOPTION IMPORT, fixing existing export files too.[HTTP]/signinand/signupeach applied the other's body-size limit; each route now uses its own. On wasm,http::*calls failed capability checks outright and carried noUser-Agent; both fixed.fetch()withredirect: "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 synchronousDEFINE EVENT). Also fixed a TiKV regression that roughly doubled write-path RPCs for multi-recordINSERT/UPSERTafter 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]TheDEFINE APIhandler'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 idiomforms, function-callsleep, keyword record ids inRELATEtargets), andRecordId::parse_simpleno 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 asCommitOutcomeUnknown- telling the client to read back rather than replay - instead of a definite "nothing was written" error.[Enterprise]BothTransactableimplementations discarded a released savepoint's undo state, mirroring the community savepoint fix.
Breaking changes
ISO GQL is enabled by default
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 / UPSERT evaluation semantics
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.
DEFINE MODULE syntax (Surrealism, experimental)
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.
Memory backend no longer supports time-travel
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.
duration::set_* functions removed
All seven duration::set_* functions were parser-advertised but never implemented - every call failed at runtime. They now produce a clean parse-time error.
Writing computed fields and functions are refused at definition time
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.
Rust SDK and embedder API changes
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>.
JavaScript package split
@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.
Our newsletter
Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks
No newer release line.
3.2
Updated Aug 3, 2026