Mastering Redis GET and SET: Basic Data Operations

Master the foundation of Redis data management with this comprehensive guide to the `GET` and `SET` commands. Learn basic string storage and retrieval, and explore essential advanced options like atomic setting (`NX`/`XX`) and integrated key expiration (`EX`/`PX`). Discover how these fundamental commands are crucial for building high-performance caching layers.

Mastering Redis GET and SET: Basic Data Operations

Redis GET and SET look almost too simple. Write a value. Read a value. In real applications, those two commands sit behind login sessions, feature flags, rate limits, cache entries, short-lived locks, and "please do not query the database again" shortcuts.

The details matter because Redis does exactly what you ask. If you overwrite a key by accident, it will not ask if you meant it. If you forget an expiration on cache data, it may live longer than the data source. If you treat a missing key the same as an empty string, your application may make the wrong decision.

The Redis Key-Value Model

Before diving into the commands, it’s important to remember that Redis operates on a simple key-value store model. Every piece of data (the value) is accessed using a unique identifier (the key). Keys are strings, and values can be various data types (strings, lists, sets, hashes, etc.). SET and GET primarily deal with the String data type, which is the most basic and frequently used type in Redis.

1. Setting Data: The SET Command

The SET command is used to assign a value to a key. If the key already holds data, the SET command will overwrite the existing value. Its basic syntax is straightforward.

Basic Syntax and Usage

The simplest form requires only the key and the value:

SET key value

Example: storing a user's display name:

127.0.0.1:6379> SET user:100:name "Alice Johnson"
OK

127.0.0.1:6379> GET user:100:name
"Alice Johnson"

Advanced SET Options: NX, XX, and Expiration

The power of SET comes from its optional arguments, which allow for atomic conditional setting and time-to-live (TTL) management. These options are vital for implementing locks and caches correctly.

Conditional Setting: NX and XX

These options control when a set operation occurs, preventing accidental overwrites or ensuring an overwrite happens only if the key exists.

  • NX (Not Exists): Only set the key if it does not already exist. This is useful for "create only" operations and simple lock patterns.

    SET my_lock_key some_unique_value NX
    
  • XX (Exists): Only set the key if it already exists. This is useful when you want to refresh a known key without creating a new one by mistake.

    SET session:token:456 new_value XX
    

B. Setting Expiration Time (TTL)

To manage memory and implement time-based caching, you can set an expiration time directly within the SET command. This is far more efficient than setting the key and then calling EXPIRE separately.

  • EX seconds: Sets the expiration time in seconds.
  • PX milliseconds: Sets the expiration time in milliseconds.
  • EXAT timestamp: Sets expiration to a specific Unix timestamp (seconds).
  • PXAT timestamp: Sets expiration to a specific Unix timestamp (milliseconds).

Example: setting a key to expire in one hour:

127.0.0.1:6379> SET cache:product:500 "Product Details" EX 3600
OK

127.0.0.1:6379> TTL cache:product:500 
(integer) 3598 

Use SET key value EX N or PX N for cache entries. It makes the write and the expiration part of one command, which avoids the common bug where the application writes a cache key and crashes before calling EXPIRE.

Combining Options

All options can often be combined for complex atomic operations:

# Set the key only if it doesn't exist, and make it expire in 60 seconds
SET my_config_setting "active" NX EX 60

2. Retrieving Data: The GET Command

The GET command retrieves the string value associated with a given key. It is one of the fastest operations Redis performs, often completing in microseconds.

Basic Syntax and Usage

GET key

Example: Retrieving the stored user name

127.0.0.1:6379> GET user:100:name
"Alice Johnson"

Handling Non-Existent Keys

If the key does not exist, GET returns a special response indicating that nothing was found:

127.0.0.1:6379> GET non_existent_key
(nil)

In application code, receiving (nil) is the standard way to determine that the data is missing, usually triggering a cache miss where the application must fetch the data from the primary source (like a database) and subsequently write it back to Redis.

Getting a Value While Changing Expiration: GETEX

The basic GET command returns only the value. It does not return the remaining TTL. If you need the TTL, use TTL key or PTTL key as a separate command.

GETEX is different: it returns the value and changes the key's expiration at the same time. That is useful for sliding-session behavior, where each read extends the session lifetime.

GETEX session:abc123 EX 1800

That reads the session value and resets the expiration to 30 minutes. Do not use this casually for normal cache reads, because every read becomes a write-like operation that changes key metadata.

3. Practical Application: Caching with GET and SET

The fundamental use case for GET and SET is implementing a simple cache-aside pattern.

Steps in application logic:

  1. Try GET product:500.
  2. If Redis returns a value, decode it and return it.
  3. If Redis returns nil, fetch the product from the primary database.
  4. Store the serialized result with SET product:500 <json> EX 300.
  5. Return the result to the caller.

This pattern can reduce database load, but it also creates a stale-data window. If the product changes in the database, Redis may still serve the old value until the TTL expires or your application invalidates the key. Choose TTLs based on how wrong the data is allowed to be, not just how much traffic you want to save.

Naming Keys Without Creating a Mess

Redis does not require a naming convention, but your future self will. A readable pattern like user:100:name, product:500:summary, or rate:user:100:login makes debugging with redis-cli much easier.

Keep keys clear and reasonably short. Saving a few bytes by naming a key u:100:n is rarely worth the confusion unless you are operating at very large scale and have measured key overhead. For most teams, consistency matters more than extreme brevity.

Be careful with user-supplied values in keys. If an email address, URL, or tenant name becomes part of the key, normalize it first. Otherwise, small formatting differences can create duplicate cache entries:

[email protected]
[email protected]
 [email protected]

Those may all represent the same user to your application but different keys to Redis.

Overwrites, Empty Values, and Nil

SET overwrites by default:

SET config:mode "safe"
SET config:mode "fast"
GET config:mode

The final value is "fast". If overwrite would be dangerous, use NX or XX.

Also distinguish a missing key from an empty value. Redis nil means the key is missing. An empty string is a real stored value:

SET user:100:nickname ""
GET user:100:nickname

Your client library may represent those differently: null, None, nil, an empty byte string, or an empty string. Check the client behavior instead of guessing.

Safe Lock Pattern, With a Warning

You will often see this pattern:

SET lock:invoice:123 "worker-7:1700000000" NX EX 30

It means "create this lock only if it does not exist, and expire it after 30 seconds." The expiration is not optional. Without it, a crashed worker can leave a lock behind forever.

For simple single-instance Redis setups, this pattern is often enough for low-risk coordination. For critical distributed locking across failures, clock drift, and multiple Redis nodes, use a well-reviewed library and understand its tradeoffs. A lock bug can become a data corruption bug.

Debugging with redis-cli

When a GET or SET path behaves strangely, check the key directly:

redis-cli GET product:500
redis-cli TTL product:500
redis-cli TYPE product:500

TYPE is useful because GET only works on string values. If the key holds a hash, list, set, or sorted set, Redis returns a wrong-type error. That usually means two parts of the application are using the same key name for different purposes.

If you need to inspect several related keys during development, SCAN is safer than KEYS on a busy production server:

redis-cli SCAN 0 MATCH 'product:500:*' COUNT 100

KEYS * can block Redis while it scans the keyspace. It is fine on a tiny local instance. It is a bad habit on production.

Choosing TTLs in Real Systems

TTL choice is a product and operations decision, not a Redis trick. A user profile cache might tolerate five minutes of staleness. A permission check may need a much shorter TTL or explicit invalidation. A feature flag might need near-immediate updates if it controls a risky rollout.

Here are three common patterns:

SET cache:product:500 "<json>" EX 300
SET session:abc123 "<json>" EX 1800
SET rate:user:100:login "1" EX 60 NX

The product cache can be a little stale. The session has a clear lifetime. The rate-limit key uses NX so the first attempt creates the window and later attempts can increment or check related keys depending on the design.

Avoid "forever cache" keys unless you have a clear invalidation path. A forever cache eventually becomes a second database, usually without the operational discipline you apply to the real database.

Serialization Details

Redis strings are binary-safe. They can hold JSON, MessagePack, compressed data, counters, or plain text. The command does not care. Your application does.

JSON is easy to inspect:

SET product:500 "{\"id\":500,\"name\":\"Desk Lamp\"}" EX 300

Binary formats can save space or CPU in some applications, but they make terminal debugging harder. Compression can help for large repeated data, but it also adds CPU cost and may hide the fact that you are caching objects that are too large.

For counters, do not read with GET, add in the application, and write with SET if multiple clients may update the same key. Use Redis atomic counter commands instead:

INCR page:view:500
EXPIRE page:view:500 86400

For first-write-with-expiry counters, use a transaction or a small Lua script if you need strict behavior. Otherwise, be clear about the race you are accepting.

Avoiding Key Collisions

Two teams using the same Redis database can accidentally reuse a key like user:100. One team stores JSON with SET; another stores fields with HSET. The next GET returns a wrong-type error, and both teams lose time.

Namespaces help:

shop:prod:user:100:profile
shop:prod:session:abc123
billing:prod:invoice:9001

You do not need painfully long keys, but include enough context to avoid collisions between environments, services, and data types. If you share Redis across applications, a naming convention is part of the interface.

When Not to Use GET and SET

Strings are the starting point, not the whole Redis model. If you frequently update one field inside a larger JSON blob, a hash may be cleaner:

HSET user:100 name "Alice Johnson" email "[email protected]"
HGET user:100 email

If you need ordered events, use streams or lists. If you need membership checks, use sets. If you need ranking, use sorted sets. Rewriting a whole serialized string for every small change is simple at first, but it can become expensive and awkward as the object grows.

A Small Checklist Before Shipping

Before you ship a new GET and SET path, ask a few plain questions.

  • What is the exact key name?
  • Can two services accidentally use the same key?
  • Should the key expire?
  • What happens when Redis returns nil?
  • Is overwrite acceptable, or should the write use NX or XX?
  • Is the value small enough to read and write as one string?
  • Can you debug the value from redis-cli if production behavior looks wrong?

These questions catch most basic Redis string mistakes before they become incidents. The command syntax is easy. The lifecycle around the key is where bugs usually hide.

GET and SET are small commands with a lot of operational weight. Use expirations for cache data, use NX or XX when overwrites matter, treat nil as a separate state, and keep key names consistent enough that someone can debug them from a terminal. Once strings start feeling cramped, move related fields into hashes and use the data structure that matches the access pattern.