Documentation for agents: an index at /llms.txt, every docs page in one file at /llms-full.txt, and Markdown for any docs page by adding .md to its URL.

Effect KitRedis

Transactions

Run commands as one MULTI and EXEC block, with optional WATCH for optimistic locking.

A transaction queues commands with MULTI and runs them together with EXEC. No other client's command runs between them. redis.transaction takes Commands.* values and returns a TransactionOutcome.

Example (Reading and Incrementing in One Transaction)

import {  } from "effect";

import { Commands,  } from "@effect-kit/redis";

const  = .(function* () {
  const  = yield* ;

  const  = [Commands.("{account:42}:name"), Commands.("{account:42}:visits")] as ;

  const outcome = yield* .();
const outcome: TransactionOutcome<readonly [Command<Option<string>, never, never, never>, Command<bigint, never, never, never>]>
if (. === "Conflict") return "conflict"; return .; });

TransactionOutcome is { _tag: "Committed", results } or { _tag: "Conflict" }. results has one Result per command, typed like a pipeline. Conflict means a watched key changed, so Redis dropped the block. It is a success value, not an error.

No rollback

If one command fails at run time, the others stay applied. EXEC does not undo them. Only the queueing step is all or nothing: a rejected command at queue time fails the transaction with reason: "queue-rejected".

Pipeline or transaction

redis.pipelineredis.transaction
Round tripsOneOne
Other clients can interleaveYesNo
Result per commandResultResult, inside Committed
WATCH conflictNot applicableConflict
Run-time error in one commandOthers still applyOthers still apply

Submission on standalone connections

On a standalone native connection, a profile of RedisProfiles.redis() or RedisProfiles.valkey() lets the client send the queued commands and EXEC together once MULTI succeeds. Other profiles send them one step at a time. Results and errors are the same either way. See Server engines.

Handling transaction errors

A transaction that cannot run fails with a TransactionError. Its reason is "queue-rejected" or "exec-rejected", and execution is "not-executed" or "unknown". Failures of the batch itself, such as a lost connection, arrive as BatchError with per-command evidence.

Example (Telling a Rejected Transaction From a Lost One)

import {  } from "effect";

import { Commands,  } from "@effect-kit/redis";

const  = .(function* () {
  const  = yield* ;

  return yield* 
    .([Commands.("orders:count")])
    .(
      .("TransactionError", () =>
        .(
          . === "not-executed" ? "safe to retry" : "check the key first",
        ),
      ),
    );
});

"not-executed" means the block never ran. "unknown" means it may have.

Optimistic locking with WATCH

WATCH marks keys before the transaction. If another client changes one before EXEC, the outcome is Conflict. WATCH belongs to a connection, so it lives on RedisSessions: sessions.watch(keys, body) leases a session, watches the keys, runs body and cleans up, even on early return or interruption. keys must have at least one key.

Example (Retrying a Read-Modify-Write on Conflict)

import { ,  } from "effect";

import { Commands,  } from "@effect-kit/redis";

const  = "{account:42}:balance";

const  = (: number) =>
  .(function* () {
    const  = yield* ;

    const  = .([], () =>
      .(function* () {
        const  = yield* .();
        const  = (.(, () => "0")) + ;

        return yield* .([Commands.(, ())]);
      }),
    );

    // Another client changed the key between WATCH and EXEC: try again
    let  = yield* ;
    while (. === "Conflict")  = yield* ;

    return ;
  });

Inside the body, session runs commands on the leased connection, so reads are under the watch. A session can also call session.watch(keys) and session.unwatch() by hand.

WATCH is not available on Upstash, which has no sessions. Transactions themselves work over HTTP, but commands with ambiguous replies (raw, custom or script commands) are rejected with an UpstashError whose reason is "encoding".