The Redis API

The Redis global gives your scripts a pooled connection (via Redisson) to a Redis server: string/hash key-value storage, lists, atomic counters, and pub/sub. Unlike GlobalMap and GlobalNotification, which only work within a single JVM, Redis is the tool for state and messaging that has to cross multiple servers in a network.

The module is optional and disabled by default. Configuration lives in redis.yml. See Configuration.

Checking availability

Redis.isAvailable() tells you whether enabled: true is set and the connection is up. Guard any script that has a hard dependency on it:

if (!Redis.isAvailable()) {
  throw new Error('Redis is not available');
}

Throwing at the top level marks the script as failed and shows up in /script list. Do not silently degrade a feature that needs cross-server state into one that only half-works per-server.

Key/value operations

Redis.set(key, value) / Redis.get(key)

Plain string storage.

Redis.set('server:lobby-1:motd', 'Welcome!');
const motd = Redis.get('server:lobby-1:motd');   // null if the key doesn't exist

Redis.setEx(key, value, seconds)

Same as set, but the key expires itself after seconds. The natural fit for cooldowns, temporary bans, or one-time tokens, without you having to clean anything up.

Redis.setEx(`cooldown:${uuid}:teleport`, 'true', 30);

Redis.exists(key) / Redis.delete(key)

if (Redis.exists(`ban:${uuid}`)) {
  player.kickPlayer('You are banned network-wide.');
}

Redis.delete(`cooldown:${uuid}:teleport`);   // true if a key was actually removed

Hash operations

A hash is a map of fields under one key, good for structured per-entity data (a player’s stats, an item’s metadata) without exploding into one Redis key per field.

Redis.hset(`player:${uuid}:stats`, 'kills', String(kills));
Redis.hset(`player:${uuid}:stats`, 'deaths', String(deaths));

const kills = Redis.hget(`player:${uuid}:stats`, 'kills');
const stats = Redis.hgetAll(`player:${uuid}:stats`);   // { kills: '12', deaths: '4' }

Values are strings on the wire, same as set/get. Parse numbers yourself with Number(...).

Atomic counters

Redis.increment(key) and Redis.decrement(key) are atomic across every server connected to the same Redis instance, which makes them the right tool for currency, cross-server unique IDs, or shared rate limits, where two servers writing to the same key from get/set would race.

const newBalance = Redis.increment(`balance:${uuid}`);
const remaining = Redis.decrement(`votes-remaining:${uuid}`);

List operations

A Redis list is an ordered collection under one key, visible to every connected server. The natural fit for cross-server queues and small ordered state that more than one server reads or writes.

Redis.listPush(`queue:${uuid}:homes`, 'spawn');     // append to the end
const homes = Redis.listGet(`queue:${uuid}:homes`); // the whole list, in order
Redis.listContains(`queue:${uuid}:homes`, 'spawn'); // true
Redis.listRemove(`queue:${uuid}:homes`, 'spawn');   // drop every occurrence

Redis.listGet(key)

Returns the entire list, in order. An empty list if the key does not exist. It supports index access and iteration like an array; call toArray() if you want a real JS array to run array methods on.

Redis.listPush(key, value)

Appends value to the end of the list. The list is created on the first push, so there is no explicit setup step.

Redis.listSet(key, values)

Replaces the entire list in one call. The old contents are deleted first, which makes this how you write a modified list back:

const homes = Redis.listGet(`queue:${uuid}:homes`);
Redis.listSet(`queue:${uuid}:homes`, homes.toArray().filter((home) => home !== 'old-base'));

Redis.listRemove(key, value)

Removes every occurrence of value from the list.

Redis.listContains(key, value)

Returns true if value is in the list.

Unlike the hash getters, every list helper touches the whole list per call, so they are O(n). Keep lists for small, ordered data; a large collection you would otherwise query belongs in Sql.

Pub/Sub

Redis.publish and Redis.subscribe broadcast between every server connected to the same Redis instance, which is what makes this the cross-server counterpart to GlobalNotification (single JVM only).

Redis.publish(channel, message)

Returns the number of subscribers that received the message, across every connected server.

Redis.publish('chat:global', JSON.stringify({ player: sender.getName(), text: message }));

Redis.subscribe(channel, handler)

handler is (channel, message) => void. Returns a listener id, needed to unsubscribe.

const listenerId = Redis.subscribe('chat:global', (channel, message) => {
  const { player, text } = JSON.parse(message);
  Bukkit.broadcast(`<${player}> ${text}`);
});

Subscriptions registered this way are automatically cleaned up when the script unloads, same guarantee GlobalNotification gives you. You do not need an unload callback for this. Unsubscribe manually only if you need to stop listening before that:

Redis.unsubscribe('chat:global', listenerId);

Worked example: cross-server chat

// features/chat/relay.js
if (!Redis.isAvailable()) throw new Error('Redis is not available');

const SERVER_NAME = 'lobby-1';

Redis.subscribe('chat:global', (channel, message) => {
  const payload = JSON.parse(message);
  if (payload.origin === SERVER_NAME) return;   // don't echo our own messages back

  Bukkit.broadcast(`<gray>[${payload.origin}]</gray> <${payload.player}> ${payload.text}`);
});

registerListener($.AsyncChatEvent, (event) => {
  Redis.publish('chat:global', JSON.stringify({
    origin: SERVER_NAME,
    player: event.getPlayer().getName(),
    text: event.message(),
  }));
});

Best practices

Do

  • Guard the module with Redis.isAvailable() at load time
  • Use increment/decrement instead of read-modify-write for anything two servers could touch at once
  • Use list operations for small, ordered, cross-server collections and queues
  • Namespace keys by feature and entity, e.g. feature:entity-id:field
  • Use setEx for anything that should expire instead of tracking expiry yourself

Do not

  • Assume get/hget succeeded without checking for null
  • Treat list operations as a queryable structure; they read and rewrite the whole list per call
  • Treat Redis as a database for data that needs querying, filtering or joins, that is what Sql is for
  • Store large blobs; Redis is for hot, small, frequently accessed state

Redis versus the other shared-state globals

  • GlobalMap / GlobalNotification for state and messaging within a single server process. No network hop, no external dependency, but invisible to every other server in the network.
  • Redis for the same shapes of problem the moment more than one server needs to see them.
  • Sql for data that needs to be queried, filtered, or joined, not just fetched by key.

See Shared state and messaging for the single-JVM globals.