Skip to main content
Swytch Documentation
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Real Transactions in Application Code

Read a value, make a decision in your application, and update several keys in one transaction. Start Swytch with --ssi to use serializable snapshot isolation (SSI): commands inside MULTI return their actual results immediately, and EXEC commits the changes together.

Lua is not required for this workflow. Keep the decision in the language your application already uses. SSI transactions are the recommended approach for new transactional logic; Lua scripts and functions are provided on a best-effort basis for Redis compatibility.

Enable SSI

Use Swytch 1.6+ with --ssi. Start a local server:

swytch redis --ssi --bind 127.0.0.1 --port 6379 --maxmemory 256mb

--ssi is a server startup option, disabled by default. It changes MULTI replies for connections to that server. Enable it on each Redis endpoint your application uses for this workflow. Existing Redis transaction helpers may require QUEUED replies; validate those clients before enabling the flag on their endpoint.

BehaviorDefault modeWith --ssi
MULTIStarts queuing commandsCaptures the transaction snapshot
Commands inside MULTIReturn QUEUEDExecute on the snapshot and return results immediately
Application decisions between commandsResults arrive at EXECUse each result to choose the next command
EXECExecutes queued commands and returns their resultsCommits pending changes and returns the results already produced
DISCARDDrops queued commandsDiscards pending changes

The flag changes when commands execute and when results arrive. It does not enable disk persistence. Choose durability and tiering separately if the data must survive losing its holders.

Read, decide, and write

Connect with redis-cli -p 6379. Seed a demonstration stock count outside the transaction:

> SET stock:demo 10
OK
> MULTI
OK
> GET stock:demo
"10"
> DECRBY stock:demo 3
(integer) 7
> GET stock:demo
"7"
> EXEC
1) "10"
2) (integer) 7
3) "7"

The first GET gives your application enough information to decide whether three items are available. If they are, issue DECRBY. Otherwise, issue DISCARD. Both decisions can happen in your normal application code.

MULTI fixes the snapshot for the transaction, including keys first read later in the transaction. Commands also see earlier writes from that same transaction. Other connections see committed state, never those pending changes.

EXEC does not run the commands a second time. It commits the transaction and returns an array containing the same results you already received, in command order. Treat every earlier result as provisional until EXEC succeeds.

There is no need to WATCH keys read inside an SSI transaction to establish isolation. WATCH remains available for observations made before MULTI; UNWATCH removes those watches. MULTI calls cannot be nested.

Use one connection for the whole transaction

Keep the connection from MULTI through EXEC or DISCARD. Read each response before making the next decision. A pool must not move individual commands between connections, and a transaction helper must not insist on QUEUED or defer all reads until EXEC.

For example, install redis with python3 -m pip install redis. This Python function uses redis-py’s low-level connection directly, so its normal Redis pipeline behavior does not hide the intermediate replies:

import time

import redis
from redis.backoff import NoBackoff
from redis.retry import Retry


def reserve_stock(key, quantity, attempts=5):
    """Return remaining stock after commit, or None if stock is insufficient."""
    if quantity <= 0 or attempts <= 0:
        raise ValueError("quantity and attempts must be positive")

    for attempt in range(attempts):
        conn = redis.Connection(
            host="127.0.0.1",
            port=6379,
            decode_responses=True,
            socket_connect_timeout=2,
            socket_timeout=2,
            retry=Retry(NoBackoff(), 0),
        )

        def command(*args):
            conn.send_command(*args)
            return conn.read_response()

        try:
            conn.connect()
            command("MULTI")
            available = int(command("GET", key) or 0)
            if available < quantity:
                command("DISCARD")
                return None

            remaining = command("DECRBY", key, quantity)
            results = command("EXEC")
            if results is not None:
                return remaining
            # A null EXEC confirms an abort. Re-read and decide again.
        finally:
            conn.disconnect()

        if attempt + 1 < attempts:
            time.sleep(0.01 * (attempt + 1))

    raise RuntimeError("stock transaction repeatedly conflicted")

Seed stock:demo to 10, then call reserve_stock("stock:demo", 3). It returns 7 after a successful commit. Calling reserve_stock("stock:demo", 8) then returns None and leaves stock at 7.

The function retries only a confirmed transaction abort. Command errors and connection failures propagate to the caller. Closing the dedicated connection discards any transaction that has not committed. In a pooled implementation, discard the transaction or close the connection before returning it to the pool.

Commit, abort, and retry

ResultMeaning and next step
An immediate command resultThe result belongs to this transaction’s snapshot. Wait for EXEC before reporting a committed update.
An array from EXECThe transaction committed; the array contains the collected command results.
Null from EXEC (nil in redis-cli, None in Python)The transaction aborted because of a conflict or failed watch. Start again at MULTI, including all reads and decisions.
EXECABORTA previous command was invalid or rejected. Pending changes were discarded; fix the cause before retrying.
A command runtime error, such as WRONGTYPEThe error is returned immediately and included in the collected results if you continue to EXEC. Use DISCARD when your operation cannot proceed.
TRYAGAIN or another storage or availability errorHandle the reported failure. Do not treat it as a successful commit; review partition behavior and your retry policy.
Connection loss while waiting for EXECThe commit outcome may be unknown. Reconcile using an application operation ID or another recovery mechanism before repeating business effects.

DISCARD and disconnect discard pending writes. Runtime errors do not automatically discard the entire transaction; applications should stop and issue DISCARD if a failed command invalidates their decision. The Python example closes its dedicated connection on such errors.

Keep transactions short. Do not hold a transaction open while waiting for user input or an external service. Send emails, charge payments, or publish messages only through an application workflow that handles commit outcomes and duplicate requests. Those external effects are not part of the Swytch transaction.

SSI uses the engine’s transaction and conflict handling. It does not remove shared-key network costs or override safe and available modes. For disconnected operation and reconciliation, read partitions and conflict reporting.

Commands with limits inside SSI

These commands are rejected inside SSI transactions because they affect state outside the transaction or enumerate the live key index:

  • FLUSHDB, FLUSHALL
  • PUBLISH
  • SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE
  • KEYS, SCAN, RANDOMKEY, DBSIZE

Commands already unsupported inside ordinary MULTI remain unsupported. Blocking commands with transaction support use their non-blocking form inside MULTI; for example, BLPOP returns null immediately when no item is available.

For a single-key update, first check whether an existing command expresses the operation. INCR, SET NX, and conditional SET options such as IFEQ can avoid a multi-command transaction. See keys and values.

Migrating Lua logic

Move reads and writes into a MULTI/EXEC block, and move branches and calculations into application code. Use the immediate replies to make each decision, then retry the whole block if EXEC returns null. An application beside its Swytch sidecar can use localhost or a Unix socket for those command round trips.

Existing Lua scripts and functions have best-effort compatibility support. Test scripts you keep against the Lua compatibility limits. Lua is optional; SSI transactions are the recommended path for new read–decide–write logic.

After trying the examples, delete the demonstration key outside a transaction:

redis-cli -p 6379 DEL stock:demo