NEW

New University Course: SurrealDB for AI Engineers

Start the course

1/3

SurrealDB 4.0

Releases. 1 beta on the 4.0 line, with the release date and full notes for each below.
Latest. 4.0.0-beta.1, released Oct 28, 2026, is the newest beta on this line and the first set of notes below.
Pre-release. A beta line: try it out, and pin a stable release for production while the line is still in beta.

This release introduces the first beta of SurrealDB 4.0. Numbers keep their size and one storage engine runs everywhere. We would love to hear how it goes. The…

Released Oct 28, 2026

4.0.0-beta.1

PRE-RELEASE

This release introduces the first beta of SurrealDB 4.0. Numbers keep their size and one storage engine runs everywhere. We would love to hear how it goes. The upgrade is one-way, so read the upgrade notes before trying it on data you care about.

Until now, every whole number in SurrealDB was a 64-bit int and every fraction a 64-bit float, so a schema had no way to say that a sensor id fits in two bytes or that an embedding only needs 32-bit precision. 4.0 brings sized numbers, from i8 and u8 up to u64, plus f16 and f32. A field declared with one stores its values at that size and turns away any value that does not fit:

DEFINE FIELD sensor ON reading TYPE u16;
DEFINE FIELD level ON reading TYPE f32;

CREATE reading:1 SET sensor = 65535, level = 1.25;
-- [{ id: reading:1, level: 1.25f32, sensor: 65535u16 }]

CREATE reading:2 SET sensor = 65536, level = 1;
-- "Couldn't coerce value for field `sensor` of `reading:2`: Expected `u16` but found `65536`"

Literals can carry their size as well, as in 255u8 or 1.5f32, and arithmetic on them fails rather than wrapping round, so 255u8 + 1u8 is an error instead of a quiet 0. SDKs that speak the new CBOR v2 protocol get the exact width back, and 3.x SDKs carry on over the original protocol.

Sized numbers · Sized number fields · CBOR v2

Snapshot isolation, the default, lets two transactions each read something the other is about to change. Two receptionists booking the last free Monday slot can both see an empty calendar, and both bookings commit. BEGIN learned to ask for a stronger guarantee:

BEGIN ISOLATION LEVEL SERIALIZABLE;

LET $booked = SELECT VALUE id FROM booking WHERE day = 'monday';
IF $booked.is_empty() {
    CREATE booking SET doctor = doctor:ana, day = 'monday';
};

COMMIT;

When two of these run at once, the second COMMIT now fails with a conflict you can retry. The in-memory engine and SurrealDS offer the level, while SurrealKV and TiKV refuse it outright rather than quietly running at a weaker one. Reads got their own option too: BEGIN READ ONLY turns away every write, and on SurrealDS, MAX STALENESS 5s lets a report read from a snapshot up to five seconds old.

Serializable isolation · Read-only transactions

Array record ids such as reading:['London', d'2026-10-01T09:00:00Z'] keep each city's readings together in time order, which makes them a natural fit for time series. Reading a slice of one, though, meant writing a record range by position. You can now name the parts of the id once and query them like ordinary fields:

DEFINE FIELD id ON reading TYPE [city: string, time: datetime];

SELECT time, temp FROM reading
WHERE city = 'London' AND time >= d'2026-10-01' AND time < d'2026-10-08';

The WHERE becomes a scan of the keys between those two points, so the query reads one city's week and nothing else. ORDER BY time DESC LIMIT 10 reads backwards from the newest reading and stops after ten, which keeps "the latest readings for London" cheap at any table size.

Naming the components of an array id

Questions like "which cities are within 400 km of London?" used to check the geometry of every record in the table. The new SPATIAL index stores geometries on a grid over the globe, so a query reads only the cells around the area it asks about:

DEFINE INDEX city_location ON city FIELDS location SPATIAL;

SELECT VALUE name FROM city
WHERE geo::distance(location, (-0.1276, 51.5072)) < 400000;

It serves INSIDE, INTERSECTS, CONTAINS and the other geometry operators as well as a radius written with geo::distance, and it works alongside other indexes, so status = 'open' AND location INSIDE $area uses both. A nearest-neighbour vector search can be narrowed to an area the same way.

Spatial indexes

On-disk storage used to come in several engines, with RocksDB on servers and IndexedDB in the browser. From 4.0, SurrealKV 1.0 is the one engine on a server, in an embedded SDK and in the browser, where it keeps the database in the Origin Private File System. Existing data comes along on its own: a rocksdb:// or indxdb:// path still opens, and SurrealKV converts what it finds there the first time it starts. To keep the original directory untouched, convert it into a new one ahead of time:

skv migrate path/to/rocksdb-database path/to/surrealkv-database
surreal db start --user root --pass secret surrealkv://path/to/surrealkv-database

SurrealKV also takes compaction requests from ALTER ... COMPACT now, which only RocksDB answered before.

Moving a RocksDB database to SurrealKV · RocksDB and IndexedDB are removed

  • Cookie sign-in: Browser apps can let SurrealDB hold their session for them. With --cookie-auth, signing in through /signin/cookie puts the token in an HttpOnly cookie that page scripts never see, with a rotating refresh cookie for record access defined WITH REFRESH. (learn more)

  • APIs with caller permissions: DEFINE API ... SECURITY INVOKER runs an endpoint with the caller's own permissions, so it returns exactly what that user could read with a direct query. (learn more)

  • Stored back-references: MATERIALIZE INTO keeps a list of the records that point at a record on the record itself, so an author's posts are one field read away. (learn more)

  • Unique nulls: UNIQUE NULLS NOT DISTINCT treats NULL as a value, so only one record can leave a unique field NULL. (learn more)

  • Keeping the rest: Destructuring learned ..., which keeps every field you did not name, so person.{ address: address.{ city }, ... } trims the address and leaves the rest alone. (learn more)

  • Automatic retries: A statement outside a transaction that loses a write conflict now runs again by itself, unless it reaches outside the database with a call such as http::get. (learn more)

  • Shaped results: Clients can ask for results that send each row's shape once and then only the values. (learn more)

  • Finer capabilities: --allow-net "*.example.com" covers every subdomain, and --allow-mcp and --deny-mcp pick out individual MCP tools. (learn more)

  • Rust and browser SDKs: The Rust SDK can call DEFINE API endpoints with Surreal::api(), and the browser build shrank by 1.9 MB. (learn more)

The 3.x to 4.0 upgrade guide covers each of these in detail.

  • One-way upgrade: Take an export before upgrading, because once 4.0 has opened a datastore, no 3.x release starts on it. (learn more)

  • SDKs first: Upgrade every SDK before the server, because 4.0 refuses the text forms of datetimes, UUIDs and durations that some earlier Python, PHP and Go SDKs send, and 3.x Rust SDKs refuse a 4.0 server. (learn more)

  • Storage conversion: Leave room on disk for a second copy of a RocksDB or SurrealKV datastore, and give health checks extra time on the first start, because the conversion runs before the server answers /health. Run skv migrate beforehand to keep the original directory. (learn more)

  • Clusters: Upgrade every node to the latest 3.3 release first, and hold off on LIVE queries and DiskANN writes until every node runs 4.0. A TiKV datastore switches to the 4.0 format only once every node is started with SURREAL_FEATURE_LEVEL=4.0. (learn more)

  • Index rebuilds: Rebuild HNSW indexes that use JACCARD or PEARSON and full-text indexes whose records were updated and later deleted, and redefine any DiskANN TYPE I8 or TYPE U8 index above 33025 dimensions. (learn more)

  • Configuration: Move SURREAL_KNN_PREFILTER_EF_BOOST to SURREAL_KNN_PREFILTER_HNSW_EF_BOOST if you tuned it for HNSW, and remove gql from --allow-experimental, which now refuses it. (learn more)

  • Rust SDK: Replace the kv-rocksdb and kv-indxdb features with kv-surrealkv, and import AolMode, SnapshotMode and SyncMode from surrealdb::opt. (learn more)

  • Behaviour changes: Check your queries against the behaviour changes in the guide. The ones most likely to matter are exact float and decimal comparison, SPLIT naming the result's field, KNN operators in UPDATE and DELETE, and writes that create a table. (learn more)

  • Destructuring accepts a trailing ..., which keeps every field the pattern did not name, on objects and on record links.

  • UPDATE and DELETE use indexes for conditions on bound parameters, such as DELETE t WHERE time < $cut, and evaluate a WHERE on stored values before decoding records.

  • Aggregates run by record users no longer derive COMPUTED fields that the query does not read.

  • A standalone statement that loses a retryable write conflict is planned and run again, up to 10 attempts in all within its timeout. Statements with an external effect, such as an http::* call, are not retried.

  • array::min() and array::max() work as GROUP BY aggregates, and array::group() returns the unique values of a group.

  • A GROUP BY projection that does not read the record is evaluated once per group, so a constant expression gives one value.

  • A group key or aggregate argument that ran a subquery, block, custom function or script and then failed returns the error instead of NONE. Nested aggregates in GROUP BY fail with Nested aggregate functions are not supported.

  • SPLIT names a field of the result, under its alias where it has one, so SELECT tags AS t FROM person SPLIT tags fails with Missing split idiom `"tags"` in statement selection.

  • SELECT * FROM ONLY [item:404] returns NONE when the record does not exist, where 3.3 failed with Expected a single result output when using the ONLY keyword.

  • Nested arrays in FROM are flattened, so SELECT VALUE $this FROM [[1, 2], [3]] returns [1, 2, 3].

  • A record id on a table that does not exist is an error rather than [] or NONE.

  • A float equals a decimal only when the decimal is exactly the float's value, so 0.1f = 0.1dec is false.

  • An array slice past the end, such as [1, 2][0..10], returns the elements it covers instead of NONE.

  • LET cannot set $this, $self, $parent, $value, $input, $before, $after or $event inside a block, loop, condition, function or closure, which fails with 'parent' is a protected variable and cannot be set. A LET used as the value of an expression, including as a branch of IF ... THEN ... END, fails with LET statements can only appear at the top level of a query or inside a block expression.

  • A top-level RETURN no longer skips the statements after it.

  • GeoJSON objects from parameters, HTTP responses or JSON fields are accepted wherever a geometry is expected, including in the geometry operators.

  • A range passed to a function() script arrives as a Range object, and a returned Range converts back.

  • BEGIN READ ONLY starts a transaction that refuses writes and returns a description of its snapshot. MAX STALENESS <duration> lets it read a published snapshot no older than the bound, and STRICT fails it when none is recent enough. Only SurrealDS publishes snapshots; every community engine reads the current state, which meets any bound. SURREAL_READ_SNAPSHOT_MAX_STALENESS_SECS (3600 by default) caps the bound, and SURREAL_READ_SNAPSHOT_FALLBACK sets the behaviour when no snapshot qualifies.

  • BEGIN ISOLATION LEVEL SNAPSHOT | SERIALIZABLE selects the isolation level. The in-memory engine and SurrealDS offer SERIALIZABLE, and SurrealKV and TiKV refuse it with The isolation level SERIALIZABLE is not available on this datastore; it offers SNAPSHOT. It cannot be combined with a MAX STALENESS above zero.

  • The begin RPC method takes read_only and isolation options, HTTP /sql takes read_only_max_staleness and read_only_strict, and the Rust SDK has Begin::isolation(), Begin::client() and Transaction::client().

  • A failing statement cancels its transaction, and LET values set in a cancelled transaction are discarded.

  • A DEFINE API handler or middleware that fails rolls back everything it wrote, including events, index entries and sequence draws, and so does a failed surrealdb.query call that a script catches.

  • Sized number types i8, u8, i16, u16, i32, u32, u64, f16 and f32 are available in fields, casts and literals (255u8, 1.5f32), with type:: conversion and type::is_ checks for each. i64 and f64 are aliases of int and float. Arithmetic on two values of the same sized type fails on overflow, and mixed types widen to the smallest type that holds both.

  • The elements of an array record id can be named in the id field's type, as in TYPE [city: string, time: datetime]. Each name reads as a field, conditions on the names become key-range scans, and ORDER BY on a following component reads in key order.

  • REFERENCE ... MATERIALIZE INTO <table>.<field> [ WHERE <condition> ] stores on each referenced record a sorted list of the records that reference it, kept current in the same transaction.

  • DEFINE FIELD OVERWRITE ... REFERENCE and ALTER FIELD on a populated field write reference keys for existing records.

  • COMPUTED, PERMISSIONS, ASSERT, VALUE and DEFAULT clauses may not have side effects, and the check follows custom functions they call.

  • A CREATE, UPSERT, INSERT or RELATE that implicitly defines a table needs permission to run DEFINE TABLE. A database VIEWER now gets IAM error: Not enough permissions to perform this action. Record users are unaffected.

  • A nested field needs a parent whose type can hold it, and DEFINE FIELD id.<part> needs id itself defined first. A view may not read its own table, and changing a table between NORMAL and RELATION checks its records.

  • A whole-record PATCH replace with a value that is not an object fails with Cannot use 5 in a CONTENT clause and leaves the record unchanged.

  • Integer overflow in math::sum is an error, and a decimal literal with more than 28 decimal places is a parse error.

  • DEFINE INDEX ... SPATIAL indexes any geometry type on a grid of cells, serving INSIDE, CONTAINS, INTERSECTS, CONTAINSANY, CONTAINSALL, ANYINSIDE, ALLINSIDE and geo::distance(...) < r, alone or combined with other indexes, and pre-filters KNN searches by area.

  • UNIQUE NULLS NOT DISTINCT makes NULL take part in uniqueness. NONE is still not compared.

  • A DEFINE INDEX or REBUILD INDEX without CONCURRENTLY commits its definition before it builds and returns once the index is online. A build that fails removes the definition afterwards, and a UNIQUE index whose build failed refuses writes. A blocking DEFINE INDEX after writes to the same table in the same transaction is refused.

  • An unchanged DEFINE INDEX OVERWRITE keeps its index instead of rebuilding it.

  • Standard and UNIQUE index builds scan in parallel batches, set by SURREAL_INDEX_BUILD_PARALLELISM, and DiskANN indexes build in parallel.

  • count() over IN [...] or OR conditions on a B-tree index is answered from the index without reading records.

  • An index records whether a column has ever held an array, so a column that never has can serve ORDER BY ... LIMIT and ranges from index order.

  • A UNIQUE index over s.* on a set field uses the whole set as its key, and s.* over a set returns a set.

  • A KNN operator in the WHERE of an UPDATE or DELETE needs a vector index on the field and is answered from it. An UPSERT with a KNN operator always fails.

  • The KNN query vector cannot depend on the record being scanned, which fails with The KNN query vector cannot depend on the record being scanned.

  • HNSW indexes using JACCARD or PEARSON rank by distance.

  • DiskANN TYPE I8 and TYPE U8 indexes are limited to DIMENSION 33025.

  • Pre-filtered KNN is tuned by SURREAL_KNN_PREFILTER_HNSW_EF_BOOST and SURREAL_KNN_PREFILTER_EXACT_COST. SURREAL_KNN_PREFILTER_EF_BOOST now applies to DiskANN only.

  • Vector measures over eight or more floats sum their terms in a different order, so results can differ in their last digits.

  • SurrealKV 1.0 is the only on-disk engine, with a new on-disk format. The RocksDB and IndexedDB engines are removed: rocksdb:// and indxdb:// paths open with SurrealKV, which converts existing data on first start, and skv migrate converts a RocksDB directory into a new one. SURREAL_ROCKSDB_* settings are ignored with a warning.

  • In the browser, the Wasm engine connects with surrealkv://name, stores data in the Origin Private File System from a dedicated worker, and copies in an IndexedDB database of the same name on first open.

  • SurrealKV supports ALTER ... COMPACT. Its grouped-commit settings have no effect.

  • The first 4.0 start applies a data migration and, on the in-memory engine and SurrealKV, switches the datastore to the 4.0 record format, after which no 3.x release can open it. TiKV keeps the 3.x format until every node runs 4.0 with SURREAL_FEATURE_LEVEL=4.0.

  • --cookie-auth with an explicit --allow-origin list enables POST /signin/cookie, /signup/cookie, /refresh/cookie and /signout, which keep the access token, and a refresh token for record access defined WITH REFRESH, in HttpOnly cookies checked against the request's origin.

  • DEFINE API ... SECURITY INVOKER runs an endpoint's handlers and middleware with the caller's permissions.

  • HTTP RPC sessions are stored in the datastore, so any node of a cluster serves them, and --durable-sessions has no effect.

  • Clients can opt in to application/vnd.surrealdb.shaped, or the shaped WebSocket subprotocol, which sends each row shape once and then only values.

  • A CBOR request the server cannot decode is answered with the decoder's reason instead of Parse error. The server refuses the text forms of datetimes, UUIDs and durations (tags 0, 9 and 13).

  • --allow-net and --deny-net accept wildcard subdomains such as *.example.com, and a *. rule now matches subdomains.

  • --allow-mcp and --deny-mcp allow or deny individual MCP tools.

  • The server logs its capability summary at startup, and warns when guest access or scripting is on.

  • --planner-strategy is deprecated, SURREAL_MAX_CONCURRENT_TASKS is removed, and --allow-experimental gql is refused.

  • surreal upgrade no longer downloads a release, because the Surreal CLI installs and upgrades the database, and surreal --version prints a shorter line.

  • Bearer tokens are checked against their grant, so tokens issued before the upgrade are refused.

  • $auth.id returns the signed-in record id without reading the record.

  • POST /import answers 422 when part of an import fails.

  • DEFINE ACCESS with a signing key is not restored from a dump, and table values in a 3.x dump are lost on import.

  • An import drops a UTF-8 byte order mark, refuses UTF-16 with an error naming the encoding, and refuses a DEFINE FIELD whose DEFAULT does not fit its TYPE.

  • In the 4.0 Docker image, /data and /logs are owned by uid and gid 65532, so a new named volume is writable without running as root.

  • The Rust SDK adds Surreal::api() for DEFINE API endpoints, honours a custom TLS configuration on grpcs:// and refuses one on grpc://, makes cloning a WebSocket client cheap, and round-trips integers above i64::MAX as decimals.

  • surrealdb::opt::SyncMode, AolMode and SnapshotMode set the durability of local engines.

  • Config::datastore_option sets storage options by their server names, path parameters reach the storage engine, Surreal::shutdown waits until storage is released, and embedded engines can allow local file: bucket folders.

  • The kv-rocksdb and kv-indxdb features are removed, parse is no longer a default feature, unstable_from_datastore needs the unstable feature, and a dropped transaction rolls back.

  • The WebAssembly build is 1.9 MB smaller.

Fixes already released in 3.3.1 to 3.3.5 are included and not repeated here.

  • Fixed GROUP BY giving different answers for equivalent constant expressions, and aggregates over the wrong kind of value returning 0 or -Infinity instead of a result or an error.

  • Fixed UPSERT ... WHERE ... RETURN NONE creating an extra empty record when the condition matched existing rows.

  • Fixed UPDATE and DELETE ... WHERE <field> IN [...] on a composite UNIQUE index changing no rows.

  • Fixed range conditions on the leading column of a composite UNIQUE index returning the wrong rows.

  • Fixed indexed reads returning duplicate rows and a wrong count() when records stored before a DEFINE FIELD ... TYPE still held arrays.

  • Fixed CONTAINS and INSIDE over an index on field.* missing records whose value is a string, object, range or geometry.

  • Fixed a .* index on a set field returning no rows for CONTAINS, CONTAINSANY, CONTAINSALL and IN.

  • Fixed CONTAINSALL over a list that mixes floats and decimals rejecting matching rows unpredictably.

  • Fixed ORDER BY ... LIMIT over an index on a column that may hold arrays returning the wrong rows or a record twice.

  • Fixed index-ordered reads and ranges over datetimes returning rows out of order or leaving some out.

  • Fixed a closed range after an equality on a compound index using only one bound, and an equality against an array or object record id scanning the table.

  • Fixed a FROM expression that yields a record range reading nothing.

  • Fixed a record id read from a string, such as type::record("value:focus:outline-none"), keeping only the first table:key.

  • Fixed closures ignoring outer parameters used only in START or TIMEOUT.

  • Fixed record::exists() deriving every computed field of the record, so an erroring computed field made it fail.

  • Fixed SHOW CHANGES FOR TABLE ... SINCE <datetime> returning [] on the memory and SurrealKV engines, and its LIMIT counting entries from other tables.

  • Fixed VERSION reads on a versioned SurrealKV store returning records deleted before that version.

  • Fixed fields typed as sets of arrays, sets or objects storing a wrapped value or failing to read back.

  • Fixed a field typed array | array<int> making its table unreadable, including on databases upgraded from 3.0 to 3.2.

  • Fixed SCHEMAFULL tables accepting undeclared nested keys under a union with a literal.

  • Fixed unions with an object, literal or any member refusing valid values for their elements.

  • Fixed ALTER FIELD accepting a type or a REFERENCE that DEFINE FIELD refuses, which made exports fail to import.

  • Fixed fields named with a keyword or needing backticks failing to define beside an implied element definition, and failing in DEFINE INDEX on SCHEMAFULL tables.

  • Fixed a COMPUTED field whose subquery selects a field of the same name failing as a cyclic dependency.

  • Fixed UPSERT of a named record and INSERT ... ON DUPLICATE KEY UPDATE evaluating their clauses against an empty record.

  • Fixed an UPSERT that conflicted with a record its own event created silently returning [].

  • Fixed math::sum wrapping on integer overflow, math::mean losing precision, and math::product and math::spread wrapping.

  • Fixed views with math::sum, math::min, math::max or math::mean refusing writes of records that lack the field.

  • Fixed DELETE on a DROP table leaving index entries and REFERENCE keys behind.

  • Fixed redefining a view disconnecting the views that read it, DEFINE TABLE OVERWRITE dropping a view's own fields, and a view that sets its own id being keyed inconsistently.

  • Fixed foreign views seeing NONE for their source table's COMPUTED fields.

  • Fixed REMOVE TABLE on an edge table leaving its edges reachable from their endpoints.

  • Fixed a repeated clause in DEFINE TABLE silently replacing the earlier one, and two index kinds in one DEFINE INDEX silently keeping the last.

  • Fixed a decimal literal with more than 28 decimal places being rounded or becoming 0.

  • Fixed a literal array or object type validating its members without converting them.

  • Fixed JSON Patch paths being split on ., so a key that contains a dot could not be patched.

  • Fixed id.x definitions not being checked against the record key, and id reading NONE in CREATE ... SET when the id is known.

  • Fixed a sequence at the top of the i64 range wrapping round instead of failing, and sequence::nextval panicking there.

  • Fixed time::from_uuid returning a wrong year for UUIDs dated before 1970.

  • Fixed a cold sequence allocator blocking every other sequence-backed allocation on the node while it loaded.

  • Fixed an index rebuilt in a transaction that failed to commit reporting ready while returning nothing.

  • Fixed index builds that could miss writes from older transactions, or apply a write twice during REBUILD INDEX.

  • Fixed a UNIQUE index whose concurrent build failed accepting duplicates.

  • Fixed a DELETE from a snapshot taken before a full-text index build finished leaving a stale entry.

  • Fixed DEFINE INDEX after REMOVE INDEX failing while document ids were still being reclaimed.

  • Fixed indexed reads and vector searches disagreeing with table scans on equal numbers of different kinds.

  • Fixed a multi-word @@ match used as a filter under OR, or in a write's WHERE, matching any of the words instead of all of them.

  • Fixed MATCHES returning nothing over record ids, record ranges and graph hops.

  • Fixed BM25 scoring a term that appears in about half the documents or more as 0.

  • Fixed an ngram analyzer missing a word longer than its maximum gram size, and search::highlight covering only the last of several predicates that share one reference.

  • Fixed a full-text index keeping a deleted record counted under a term it kept through an update, which lowered BM25 scores. Existing indexes need REBUILD INDEX.

  • Fixed a KNN query whose vector is only known at run time returning no rows or searching with the wrong vector.

  • Fixed JACCARD and PEARSON vector searches ranking the least similar records first, and integer vector distances overflowing.

  • Fixed KNN queries returning no rows while a vector index is still being built CONCURRENTLY.

  • Fixed a DiskANN search stalling compaction and later searches under write load.

  • Fixed surreal import losing INSERT batches with a write conflict on a table whose DiskANN index is defined before its data.

  • Fixed exports of union types that imply element definitions failing to import.

  • Fixed exports writing a view before its source table.

  • Fixed table values in a record exporting as a bare name that imported as NONE.

  • Fixed surreal export to standard output mixing log lines into the dump, and surreal export failing against an HTTP/2 endpoint such as SurrealDB Cloud.

  • Fixed --import-file retrying a file that does not parse for a minute and skipping a file without USE.

  • Fixed one writer's live-query notifications arriving out of commit order.

  • Fixed a LIVE SELECT registered during an in-flight write missing that write.

  • Fixed writes by an ASYNC event never reaching live-query subscribers.

  • Fixed concurrent LIVE SELECT and KILL on one table failing with a transaction conflict.

  • Fixed clients dropping KILLED and ERROR live notifications, which left the stream open and silent.

  • Fixed live notifications stopping after REMOVE TABLE under the router live query engine.

  • Fixed a WebSocket request that fails to decode being answered without its id, which left SDK calls waiting.

  • Fixed JSON RPC strings that begin with a UUID or a datetime being turned into that value.

  • Fixed JSON values in /sql query parameters being bound as strings.

  • Fixed a RocksDB commit stall freezing the whole server.

  • Fixed memory-sized defaults ignoring a cgroup limit set below the root, such as a systemd MemoryMax.

  • Fixed surreal mcp writing log lines to standard output, and MCP tool schemas that some model providers reject.

  • Fixed JWTs whose nbf is slightly ahead of the server clock being rejected, and HTTP clients with an expired token being unable to sign in.

  • Fixed database-level BEARER grant tokens being refused for every operation.

  • Fixed a batch stopped by --query-timeout never appearing in the slow query log.

  • Fixed surreal sql exiting with 0 after a failed statement in piped input.

  • Fixed a SurrealKV commit with sync=every being visible to other transactions before it was durable.

  • Fixed a datastore reading every process on the host at startup.

  • Fixed required literal-union fields being exposed as nullable, tables without fields producing an empty update input, GRAPHQL_ALIAS on nested fields being ignored, and a dropped subscription leaving its LIVE query behind.

  • Fixed a KILL statement being left out of query results in the Rust SDK, which shifted later statement indexes.

  • Fixed tx.commit() on an embedded engine returning an internal error instead of TransactionConflict.

  • Fixed live query streams from a cloned client ending early, and every dropped authenticated HTTP client leaking a server session.

  • Fixed any::connect creating a directory with percent-encoding left in the path.

  • Fixed #[derive(SurrealValue)] reading a unit variant only in its object form, and #[surreal(crate = ..., tag = ...)] failing to compile without Value in scope.

  • Fixed REMOVE MODEL deleting the model file before the transaction committed, and a refused model import leaving its file behind.

  • Fixed a SurrealDS 1.0 node refusing every later transaction as Unavailable after a write conflict, until the process restarted.

  • Fixed a single-node SurrealDS 1.0 database refusing all writes after one failed disk write while still reporting ready.

  • Fixed SurrealDB hanging at startup when a single-node SurrealDS 1.0 log was damaged.

  • Fixed reads on a SurrealDS 1.0 replica failing under sustained writes.

Upgrade or install

Get SurrealDB v4.0.0-beta.1

Upgrade a Cloud instance from SurrealDB Studio, or copy the command for your platform.

Upgrade a Cloud instance to v4.0.0-beta.1 in four steps:

  1. Select your organisation and instance
  2. On the dashboard, select “Upgrade”
  3. The instance updates and restarts automatically
Full install guide

GET STARTED

Start building with SurrealDB. One data layer for AI, from object storage to agent memory.

ISO 27001
AICPA SOC 2 Type 2
Cyber Essentials
GDPR
Cyber Essentials Plus