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

  1. Drop the user-defined tablespace and move objects to pg_default/pg_global, then retry the writes
  2. Audit for `CREATE TABLESPACE` / `SET default_tablespace` in tenant sessions and remove them
  3. If you control ingestion, skip-and-log these keys only with an explicit policy — silently dropping breaks consistency
  4. 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

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


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/cf1b0123db4d0250. Report an issue: GitHub.