Skip to main content

Overview

Transactions in Upstash Redis allow you to execute multiple commands atomically. All commands in a transaction are serialized and executed sequentially, guaranteeing that they run as a single isolated operation.
The multi() method creates a transaction pipeline. Unlike regular pipelines, transactions guarantee atomicity - either all commands succeed or none do.

Creating a Transaction

Use the multi() method to create a transaction:
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

const transaction = redis.multi();

Basic Usage

Chaining Commands

You can chain commands together and execute them atomically:
const result = await redis.multi()
  .set('user:1:name', 'Alice')
  .set('user:1:email', '[email protected]')
  .set('user:1:age', 30)
  .exec();

console.log(result); // ['OK', 'OK', 'OK']

Building Transactions Dynamically

You can build transactions incrementally before executing:
const transaction = redis.multi();

transaction.set('key1', 'value1');
transaction.incr('counter');
transaction.get('key1');

const results = await transaction.exec();
console.log(results); // ['OK', 1, 'value1']

Type Safety

When chaining commands, TypeScript can infer return types:
const [setResult, incrResult, getResult] = await redis.multi()
  .set('key', 'value')
  .incr('counter')
  .get<string>('key')
  .exec();

// setResult: 'OK'
// incrResult: number
// getResult: string | null
For dynamic transactions, you can specify types manually:
const transaction = redis.multi();
transaction.get('user:name');
transaction.get('user:age');

const results = await transaction.exec<[string | null, number | null]>();
const [name, age] = results;

Error Handling

Default Behavior

By default, if any command fails, the entire transaction throws an error:
try {
  await redis.multi()
    .set('key', 'value')
    .hget('key', 'field') // This will fail - key is a string, not a hash
    .exec();
} catch (error) {
  console.error('Transaction failed:', error);
}

Keep Errors Mode

To handle errors individually, use the keepErrors option:
const results = await redis.multi()
  .set('key', 'value')
  .hget('key', 'field') // This will fail
  .get('key')
  .exec({ keepErrors: true });

results.forEach((item, index) => {
  if (item.error) {
    console.error(`Command ${index} failed:`, item.error);
  } else {
    console.log(`Command ${index} result:`, item.result);
  }
});

Practical Examples

Atomic Counter Update

async function incrementUserScore(userId: string, points: number) {
  const [newScore, timestamp] = await redis.multi()
    .incrby(`user:${userId}:score`, points)
    .set(`user:${userId}:last_update`, Date.now())
    .exec();
  
  return { score: newScore, updated: timestamp };
}

Batch User Creation

interface User {
  id: string;
  name: string;
  email: string;
  created: number;
}

async function createUser(user: User) {
  const transaction = redis.multi();
  
  transaction.hset(`user:${user.id}`, {
    name: user.name,
    email: user.email,
    created: user.created,
  });
  
  transaction.sadd('users:all', user.id);
  transaction.sadd(`users:by_email:${user.email}`, user.id);
  
  const [hashResult, addToAll, addToEmail] = await transaction.exec();
  
  return hashResult === 1; // Returns true if user was created
}

Transfer Between Accounts

async function transferFunds(fromAccount: string, toAccount: string, amount: number) {
  const results = await redis.multi()
    .decrby(`account:${fromAccount}:balance`, amount)
    .incrby(`account:${toAccount}:balance`, amount)
    .lpush('transactions', JSON.stringify({
      from: fromAccount,
      to: toAccount,
      amount,
      timestamp: Date.now(),
    }))
    .exec();
  
  const [fromBalance, toBalance, _] = results;
  
  return {
    fromBalance,
    toBalance,
  };
}

Complex Data Updates

async function updateProduct(productId: string, updates: Record<string, any>) {
  const transaction = redis.multi();
  
  // Update product data
  transaction.hset(`product:${productId}`, updates);
  
  // Update last modified timestamp
  transaction.hset(`product:${productId}`, { lastModified: Date.now() });
  
  // Increment version
  transaction.hincrby(`product:${productId}`, 'version', 1);
  
  // Add to updated products set
  transaction.sadd('products:recently_updated', productId);
  
  const [updateResult, timestampResult, version, addResult] = 
    await transaction.exec();
  
  return { version, updated: true };
}

Transaction Length

You can check how many commands are in a transaction before executing:
const transaction = redis.multi();

for (let i = 0; i < 10; i++) {
  transaction.set(`key:${i}`, i);
}

console.log(transaction.length()); // 10

const results = await transaction.exec();
console.log(transaction.length()); // Still 10 after execution

Important Notes

Transactions guarantee atomicity but not isolation during execution. Other clients’ commands can interleave with individual commands within the transaction. For conditional execution based on watched keys, use Redis WATCH/MULTI/EXEC pattern (not directly supported via REST API).

Differences from Redis MULTI/EXEC

  • Commands are executed via HTTP REST API, not native Redis protocol
  • All commands are sent in a single HTTP request to the /multi-exec endpoint
  • WATCH/DISCARD commands are not supported
  • Commands execute atomically on the server side

See Also

  • Pipelines - Send multiple commands without atomicity guarantees
  • Error Handling - Handle errors in your Redis operations

Build docs developers (and LLMs) love