neondatabase/neon · error
Unsupported key decoded at LSN {}: {}
Error message
Unsupported key decoded at LSN {}: {} What it means
While serializing decoded WAL records per shard, `serialized_batch` maps each block to a relation key and validates it with `is_valid_key_on_write_path()`, which requires the key be i128-representable — concretely that the tablespace-OID field fits u16 or equals the sentinel values 0xFFFFFFFF/0x22222222. A violation means the relation lives in a user-defined tablespace, which Neon storage cannot ingest on the write path.
Source
Thrown at libs/wal_decoder/src/serialized_batch.rs:164
for (shard, record) in shard_records.iter_mut() {
assert!(record.batch.is_empty());
let estimate = Self::estimate_buffer_size(&decoded, shard, pg_version);
record.batch.raw = Vec::with_capacity(estimate);
}
for blk in decoded.blocks.iter() {
let rel = RelTag {
spcnode: blk.rnode_spcnode,
dbnode: blk.rnode_dbnode,
relnode: blk.rnode_relnode,
forknum: blk.forknum,
};
let key = rel_block_to_key(rel, blk.blkno);
if !key.is_valid_key_on_write_path() {
anyhow::bail!(
"Unsupported key decoded at LSN {}: {}",
next_record_lsn,
key
);
}
for (shard, record) in shard_records.iter_mut() {
let key_is_local = shard.is_key_local(&key);
tracing::debug!(
lsn=%next_record_lsn,
key=%key,
"ingest: shard decision {}",
if !key_is_local { "drop" } else { "keep" },
);
if !key_is_local {
if shard.is_shard_zero() {View on GitHub (pinned to 8f60b04da4)
Solutions
- Drop the user-defined tablespace and move objects to pg_default/pg_global, then retry the writes
- Audit for `CREATE TABLESPACE` / `SET default_tablespace` in tenant sessions and remove them
- If you control ingestion, skip-and-log these keys only with an explicit policy — silently dropping breaks consistency
- Long term, track Neon's tablespace support status rather than working around the check
Example fix
-- before: tenant uses a user-defined tablespace CREATE TABLESPACE fastdisk LOCATION '/mnt/fast'; CREATE TABLE t(i int) TABLESPACE fastdisk; -- after: keep objects in the default tablespace CREATE TABLE t(i int);
Defensive patterns
Strategy: validation
Validate before calling
let key = rel_block_to_key(rel, blk.blkno);
if !key.is_valid_key_on_write_path() {
// reject before serialization: typically a user-defined tablespace
tracing::warn!(%key, "key not writable; check tenant for user-defined tablespaces");
continue;
} Type guard
fn is_writable_key(key: &Key) -> bool {
key.is_valid_key_on_write_path()
} Prevention
- Forbid CREATE TABLESPACE and default_tablespace changes on Neon tenants
- Audit migrated workloads for tablespace usage before ingesting their WAL
- If skipping invalid keys, do it with an explicit, logged policy — silent drops break consistency
When it happens
Trigger: A tenant executes DDL/DML on objects in a user-defined tablespace (`CREATE TABLESPACE ... LOCATION ...` followed by placing objects there); the resulting WAL blocks decode to keys whose field2 (tablespace OID) exceeds 0xFFFF, failing the check at that LSN.
Common situations: Migrating legacy postgres workloads that rely on user-defined tablespaces into Neon; running tools that create tablespaces for data layout; test suites exercising tablespace DDL.
Related errors
- Unknown RMGR {} for Heap decoding
- Unknown WAL record type for Neon RMGR: {}
- Neon RMGR has no known compatibility with PostgreSQL version
- only unsharded tenants are supported at this time: {}
- Error: {:?}
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/cf1b0123db4d0250.
Report an issue: GitHub.