skip to content

Redis has both FCALL and FCALL_RO for invoking server-side functions. What is the difference, what must the function declare for FCALL_RO to be accepted, and why would you use it?

level: seniorimportance: should knowfreq 24%

answer

  1. FCALL_RO needs the no-writes flag
  2. no-writes ⇒ runnable on replicas
  3. Writing function that wrote = unkillable
  4. Other flags: allow-oom, allow-stale, no-cluster
  5. Read-only ≠ cheap; still blocks the server

basics

~20 s

FCALL_RO only runs functions registered with the no-writes flag; any write inside them is rejected. Because they cannot mutate data, they may run on replicas and during states where writes are refused — useful for scaling reads and for read-only ACL grants.

solid answer

~50 s

`FCALL` runs any registered function. `FCALL_RO` is accepted **only** for functions registered with the `no-writes` flag, and Redis enforces read-only behaviour: attempting a write command inside raises an error. That guarantee is what buys you things: - a replica will execute an `FCALL_RO`, so read-heavy computed lookups can be scaled out and routed by a client that sends reads to replicas; - it works when the primary refuses writes — for example under `min-replicas-to-write` restrictions — and combines with `allow-stale` for stale-replica reads; - ACLs can grant read-only script execution without granting write ability; - `FUNCTION KILL` can terminate a long-running `no-writes` invocation, because nothing has been mutated; a writing function that has already performed a write is unkillable and forces `SHUTDOWN NOSAVE`. Other registration flags shape execution too: `allow-oom` (run when maxmemory is exceeded), `allow-stale`, and `no-cluster` (refuse to load in cluster mode).

code

lua · 11 lines
lua
#!lua name=reports

local function top_n(keys, args)
  return redis.call('ZREVRANGE', keys[1], 0, tonumber(args[1]) - 1, 'WITHSCORES')
end

redis.register_function{
  function_name = 'top_n',
  callback = top_n,
  flags = {'no-writes'}
}

go deeper

for a junior

Know that FCALL_RO exists, requires the no-writes flag, and refuses to write.

for a middle

Explain that the flag is declared at registration, is enforced at runtime, and is what makes replica execution possible.

for a senior

Bring in operations: replica read routing, execution while writes are refused, ACL separation, and above all that only a non-writing invocation is killable.

for a principal

Discuss it as an API design decision — split read and write paths into separate functions so the hot path stays routable and recoverable, and treat the flag set as part of the library's published contract.

## Two entry points, one guarantee Redis 7 exposes `FCALL fname numkeys …` and `FCALL_RO fname numkeys …`. They invoke the same registered Lua callback. The difference is a **contract**: `FCALL_RO` is only accepted if the function was registered with the `no-writes` flag, and while running under that flag any attempt to execute a write command from Lua fails with an error rather than mutating data. Registration looks like this: ``` redis.register_function{function_name='top_scores', callback=top, flags={'no-writes'}} ``` Without the flag, `FCALL_RO top_scores …` is rejected outright — Redis will not "try it and see". This is deliberate: the server cannot statically analyse arbitrary Lua, so it relies on the author's declaration plus runtime enforcement of it. ## Why a read-only declaration is worth having **Replica execution.** A replica must never originate a write; a normal `FCALL` is therefore not runnable there. A `no-writes` function is, so `FCALL_RO` lets you push computed reads — aggregations, filtered lookups, multi-key joins done in Lua — to replicas and scale read throughput. Client libraries that already do read-from-replica routing can route `FCALL_RO` the same way they route `GET`. **Availability of reads when writes are blocked.** A primary may be refusing writes: `maxmemory` reached with a write-refusing policy, or `min-replicas-to-write` unsatisfied. Read-only functions still work. Pair with `allow-oom` if the function must run even under memory pressure (only sensible for functions that genuinely allocate nothing significant), and with `allow-stale` if a replica should serve it even when its link to the primary is down and `replica-serve-stale-data` semantics apply — a stale-flagged function may only touch commands that are themselves allowed on stale data. **Security and blast radius.** ACL rules can permit `FCALL_RO` and deny `FCALL`, giving reporting or analytics clients server-side compute without the ability to change data. That is much stronger than trusting that a particular script "doesn't write". **Killability.** This is the operational reason seniors care. A function or script that has already performed a write cannot be aborted, because Redis has no rollback — killing it mid-way would leave a partial effect that has possibly already been propagated. So `FUNCTION KILL` refuses, and the only way out of an infinite loop after a write is `SHUTDOWN NOSAVE`, which discards unsaved data. A `no-writes` function has, by construction, produced no effect, so `FUNCTION KILL` can terminate it cleanly once `busy-reply-threshold` has elapsed. Declaring `no-writes` where it is true is therefore a resilience measure, not decoration. ## Replication consequence A read-only function produces no effects, so nothing is propagated to replicas or the AOF from it. A writing function propagates the write commands it executed (effects replication). That means `FCALL_RO` on a replica is genuinely local work with no divergence risk — the same input can even yield different bytes on different nodes without corrupting anything, because nothing is written. ## The other flags - `allow-oom` — permits the call when the server is over `maxmemory`. Without it, Redis refuses to start functions that may write once memory is exhausted. Use only when the function cannot grow the dataset. - `allow-stale` — permits execution on a replica with a broken link to the primary when stale-data serving is restricted; combine with `no-writes`, and inside the function only stale-safe commands may be called. `redis.setresp` and command availability differ here, so keep such functions minimal. - `no-cluster` — the library refuses to load on a node running in cluster mode; used for logic that assumes a single keyspace. ## Practical guidance Flag every genuinely read-only function `no-writes` by default. Do not flag a function that writes "sometimes" — a single conditional write makes the declaration false and Redis will error at exactly the wrong moment, in the rare branch, in production. If a function has a read fast path and a rare write path, split it into two functions and let the caller pick, so the common path stays replica-routable and killable. Finally, remember `FCALL_RO` limits *writes*, not *cost*. A read-only function that scans a million-element collection still blocks the whole server for the duration — on a replica, that stalls every reader on that node.

  • A function is stuck in an infinite loop. What can you do, and how does the no-writes flag change the answer?
    After busy-reply-threshold the server starts replying BUSY to other clients and accepts FUNCTION KILL. If the function has not performed a write — which a no-writes function never can — FUNCTION KILL terminates it and the server recovers. If it already wrote, Redis refuses to kill it because there is no rollback and a partial effect may already be propagating; the only remedy is SHUTDOWN NOSAVE, losing everything since the last save.
  • Can you rely on FCALL_RO to make a function safe to run on a replica under heavy load?
    Safe from a correctness standpoint, yes — it cannot mutate data or diverge the replica. But it is not free: the function occupies the replica's single command-processing thread for its whole duration, so an expensive read-only function stalls every other reader on that node. Keep such functions bounded in the number of elements they touch.

saying these in an interview costs you the question

  • Thinking FCALL_RO silently downgrades a writing function to read-only instead of rejecting it
  • Flagging a function no-writes when it writes on a rare branch
  • Assuming read-only means cheap or non-blocking
  • Believing FUNCTION KILL can stop any runaway function regardless of writes
  • Confusing no-writes with an ACL — the flag is a declaration, ACLs are the permission layer

context