Redis values are bytes: a byte-exact key editor

How SQLly's Redis key view pages, edits and decodes values without turning 00014 into 14: exact commands first, KEEPTTL, hex edits, codecs and live tests.

By

The short version

A Redis value is bytes. Everything SQLly shows you on top of those bytes is presentation.

Opening a Redis key in SQLly now opens a key view: a header strip with the key's type, TTL, encoding, memory and length, the value in pages, and the edits that type allows. Every edit shows the exact command before it runs. Under all of it is one rule: the bytes that come out of Redis are the bytes that go back in. 00014 stays 00014, an empty string stays empty, and a value that isn't text is shown and edited as hex.

Why one rule

Redis has no column types. GET, HGETALL and LRANGE all return bulk strings, and whether those bytes are a zip code, a gzip blob or a MessagePack document is up to the application that wrote them. A GUI that reads 00014 as a number has changed your data the moment it writes it back or exports it as 14.

A reported issue against Redis Insight, #6269, describes hash field values with leading zeros, such as 00014, being displayed as numbers in its Browser view. Any tool that infers types from text can make that mistake. We designed SQLly so it can't, and then wrote tests to prove it.

The key view

Double-click a key in the explorer, or right-click it and choose Open Value. The view opens on the key's own server and logical database, whichever tab is active. Redis Key View… in the command palette opens the same view with a box for a key name.

The header strip

The strip comes from one virtual command, SQLLY.KEYINFO <key>, which is made only of read-only probes: TYPE, TTL and OBJECT ENCODING pipelined, then MEMORY USAGE … SAMPLES 5, then the length command for the type . A server or ACL user that can't run MEMORY USAGE gets not measured, not an error. A key that has disappeared answers with type none, so the view can say the key is gone instead of failing.

Paged readers and "showing N of M"

A hash with 48,213 fields should not become a 48,213-row surprise, and it shouldn't be silently cut off at some fixed range either. The view reads a hash, list, set, sorted set or stream 1,000 fields, elements, members or entries at a time, and the title says how much you're looking at: showing 1,000 of 48,213 fields. Load more appends the next page, and Refresh starts again from the beginning. Strings longer than 1 MB load in 1 MB windows and report their full length. A JSON document loads whole.

Each page comes from a paged reader (SQLLY.HPAGE, ZPAGE and so on) that pairs a bounded read such as HSCAN or ZRANGE … WITHSCORES with the key's length. Values, fields, members and scores stay bulk strings: text when they are valid UTF-8, bytes otherwise. Nothing is parsed into a number.

Screenshot to comeredis-key-view-hash-paged.webp — a hash key in the key view with the header strip, the "showing 1,000 of N fields" title and Load more, and one field selected so its value shows under the grid
A large hash in the key view: type, TTL, encoding, memory and length up top, the first page of fields below, and the count of what is on screen.

Editing, one exact command at a time

Each type offers the edits it allows, and every form shows the command it will run, updated as you type. Nothing runs until you press Run.

TypeButtonsWhat runs
stringEdit value…SET key value KEEPTTL (KEEPTTL ticked by default when the key has an expiry)
hashAdd field…, Edit selected…, Delete selected…HSET key field value / HDEL key field
listSet selected…, Push to head…, Push to tail…, Remove selected…LSET key index value / LPUSH / RPUSH / LREM key 1 value
setAdd member…, Remove selected…SADD key member / SREM key member
sorted setAdd member…, Edit score of selected…, Remove selected…ZADD key score member / ZADD key XX score member / ZREM key member
JSONEdit document…JSON.SET key $ document
streamEntries, Groups, Consumers and Pending tabssee the streams post

KEEPTTL by default. A plain SET clears a key's expiry. Editing a cached value in a GUI shouldn't quietly make it permanent, so when the key has a TTL the string form ticks Keep the key's expiry (KEEPTTL) for you. Untick it and the form says "Without KEEPTTL a SET also clears the key's expiry." The preview reads SET k 00014 KEEPTTL or SET k 00014, whichever you'll actually get.

LREM removes by value. Redis has no "delete at index", so Remove selected runs LREM key 1 value, and the form says it removes by value, not position.

Hex for values that aren't text

A value that isn't valid UTF-8 is shown as hex, and its edit form switches to hex mode, so you edit the bytes themselves. You can tick Value is hex (bytes, e.g. ff 00 0a) yourself for any value. The command preview writes binary arguments as redis-cli escapes, the same spelling you would type:

HSET session:9f2c token "\xff\x00\x0a"

The reviewed write path

The key view carries the same guards as a query tab:

  • A connection marked read-only refuses every edit and says why.
  • A production connection whose writes are locked refuses too, until you unlock writes from the status-bar chip. The lock is read again right before the command runs, because an unlock can expire while the view sits open.
  • When the connection's safety settings would make a query tab confirm a data change (an unlocked production connection, or Confirm data changes on the connection), Run becomes Confirm Run and needs a second click on the command shown.

After a successful edit the key reloads, so you see what the server holds now.

Screenshot to comeredis-key-view-edit-keepttl.webp — the Edit value form on a string key with an expiry, KEEPTTL ticked, the value 00014 typed, and the preview SET … 00014 KEEPTTL above a Confirm Run button
Editing a string that expires: KEEPTTL is ticked for you, and the exact command waits for a confirming click.

Display is a lens: the value codecs

Below the grid, the selected value has a format picker, and the value viewer has the full menu: Auto, UTF-8, JSON, Hex, MessagePack, Base64, Protobuf, gzip, zstd, LZ4, deflate, PHP, Java and Python pickle. Whatever you pick, the bytes in the key never change. Formats only change what you see.

  • Compression (gzip, zstd, LZ4, deflate) decompresses, then shows the result as whatever it contains, so a gzip-wrapped JSON document opens as a JSON tree. One layer is unwrapped per pick. zstd isn't available in the browser edition.
  • Base64 (standard or URL-safe, padded or not) decodes, then the decoded bytes get the same treatment.
  • Protobuf without a schema: field numbers become keys, and each value shows as a number, text, a nested message or bytes. Field names need the schema, so you don't get them.
  • PHP serialize() output decodes to JSON, objects included.
  • Java and Python pickle are recognised and summarised: classes, opcodes, globals and strings. They are never rebuilt. Rebuilding a pickle means running its instructions, and a database viewer has no business doing that.
  • MessagePack decodes to JSON, with timestamps as {"$timestamp": "…"} and colliding map keys kept as {"$map": […]} so no entry is lost.

Auto sniffs leading bytes and structure (compression magic numbers, Java and pickle headers) and names what it found beside the menu, for example "Auto: gzip → JSON (1,234 → 5,678 bytes)". The structural guesses are conservative on purpose. The whole value must parse, and Base64 is only guessed for 16 or more characters that decode to something readable, so user:42 or a hex id stays text.

Every decoder treats the value as untrusted input. Decompressed output is capped at 16 MB, and a gzip trailer, zstd frame header or LZ4 prefix that already claims more than that is refused before decompression starts. Protobuf, PHP and MessagePack nesting stops at 128 levels. Every length inside a value is checked against the bytes actually present, and a truncated stream is reported as not decodable instead of being shown half-decoded.

The choice is remembered per key across restarts (the 2,000 most recently chosen keys), so a compressed payload reads correctly every time. Choosing Auto forgets it. Details are on Value formats.

Screenshot to comeredis-value-formats-gzip-auto.webp — the value viewer on a gzip-compressed JSON value with Format set to Auto and the note "Auto: gzip → JSON (… → … bytes)" beside the menu, the JSON tree below
Auto recognises a gzip-wrapped JSON document by its leading bytes and says what it did. The stored bytes are unchanged.

Proving it: the byte-exact suite

"We never coerce values" is easy to say. So we wrote a live test suite around the values that break tools that do:

  • 00014, a number with leading zeros
  • the empty string
  • a single NUL byte
  • 0xff 0xfe, which is not valid UTF-8
  • a 40-digit integer, far past any 64-bit type
  • 1e309, a float that overflows to infinity if anything parses it
  • a 100 KB string and a 2 MB string

Each value is written with SET through the script path, quoted the way the editor and dialogs quote it. It is read back through the streaming path and the materialized path, and then with a raw GET to prove the server holds exactly those bytes. Every cell must come back as text (valid UTF-8) or bytes (anything else), never as an integer, a float or a null. The same values are used as key names in the key browser listing, and as field names and values through HGETALL, the empty field name included. They also go through the Pub/Sub viewer, where they arrive byte-exact up to the viewer's 64 KiB stored cap. Above the cap, the payload is stored as the exact 64 KiB prefix, the full length is recorded and the truncation is marked.

On 2026-10-19 the suite passed live against Redis 7.4.11, Redis Stack, Valkey 8.1.10, KeyDB, DragonflyDB df-v2.0.2 and a TLS-only Redis. In exports, from a Redis string column, 00014, the 40-digit integer, 1e309 and 1e5 export as quoted text to JSON and SQL scripts, as the literal digits to CSV, and as inline strings in Excel. The rule is the column's type, not what the digits look like.

You can try the classic case in a query tab on any Redis 7 server:

HSET zip:test code 00014
HGET zip:test code
SET bin:test "\x00\xff"
STRLEN bin:test

HGET shows 00014, text in the grid. STRLEN says 2, and opening bin:test shows the hex 00 ff.

Rename, copy and export, still byte for byte

The key menu also has Rename… (RENAMENX, or RENAME if you tick Replace), Copy to Database… (COPY key key DB n, Redis 6.2 and later) and Copy to Connection… (DUMP on this connection, RESTORE on the one you pick, expiry included). Each one shows the exact command and respects read-only and production locks on both ends.

For whole folders, Export Folder as Command File… (or Export as Command File… on a database) writes the commands that rebuild the keys: SET, HSET, RPUSH, SADD and ZADD in lines of up to 500 members, one XADD per stream entry with its original id, JSON.SET, then PEXPIRE for keys with a TTL. Binary key names and values are written as quoted "\xHH" escapes, so Import Command File… on an empty database gives back the same bytes and the same number of keys. A few lines of an export look like this (illustrative):

SET "bin:\xff\x01" "\x00\xff"
HSET zip:test code 00014
PEXPIRE zip:test 86400000

The export only reads, so it works on read-only connections. It stops at the key limit you choose (10,000 by default) and at 8 MB, the import's file limit, so any export can be imported again. A stopped export ends with a # partial line, and types the import can't rebuild are listed as # skipped comments rather than left out silently.

Screenshot to comeredis-export-command-file-binary.webp — a .redis file produced by Export Folder as Command File… opened in a text editor, showing a binary key and value written as "\xHH" escapes and the closing completeness line
An exported folder: binary names and values as escapes, so the import rebuilds the exact bytes.

What isn't done

  • Strings over 1 MB aren't edited in the view. Read them in windows, and change them with SET or SETRANGE in a query tab.
  • There is no editor for vector sets yet.
  • Java and pickle payloads are summarised, not decoded into field values, and Protobuf has no field names without a schema.
  • Stream consumer groups aren't exported, only the entries. Importing on top of existing keys appends to lists and streams, so import into an empty database.

The full reference is on Key view, Value formats and Bulk actions. To try it on your own keys, download SQLly, connect to a Redis or Valkey server, and double-click a key.