Roblox MessagingService and Cross-Server Communication: Building Global Events That Reach Every Server

When you fired your last global announcement, did every live server actually receive it? If you cannot answer that with a number, your cross-server layer is running on faith.
MessagingService is the only first-party way to push data from one Roblox server to the others in the same universe, and it is deliberately thin. You get a topic, a payload, and a callback — plus roughly the delivery guarantees of a postcard.
That is fine for a chat relay and dangerous for a timed live event with rewards attached. What follows is the quota shape you are budgeting against, how to design topics and payloads that survive it, four fan-out patterns, and how to reconcile with Roblox DataStore state when a message never lands.
What MessagingService Actually Guarantees
The API surface is small. MessagingService:PublishAsync takes a topic string and a payload, while MessagingService:SubscribeAsync takes a topic and a callback and returns a connection you are responsible for disconnecting.
The callback receives a table with two fields that matter: the decoded payload and a Sent timestamp recorded at publish time. That timestamp is the only latency instrumentation you get for free, and most teams never read it.
Does MessagingService guarantee delivery?
No. MessagingService is best-effort: messages can be dropped, delayed, or arrive out of order. Treat every message as a hint that state changed, and keep the authoritative copy in a DataStore or MemoryStore.
Roblox does not promise delivery, ordering, or an upper bound on latency. In practice a broadcast lands across most of a universe inside a second, and in practice is not a guarantee you can build a reward drop on.
Keep in mind that a server which starts thirty seconds after your broadcast never had a subscription open, so it did not miss the message — it was never a candidate to receive it. That single fact drives most of the reconciliation design later in this guide.
Note also that this is strictly a server-to-server channel. Getting the resulting state down to players is a separate Roblox replication concern with its own bandwidth budget.
The Quota Shape You Are Budgeting Against
MessagingService is metered in several dimensions, and each one fails differently under load. Roblox publishes the current constants, so treat the table below as the shape of the budget and re-check the exact values before you tune against them.
| Limit | Documented ceiling | What it actually constrains |
|---|---|---|
| Message size | 1 KB after serialization | How much state fits in a single publish |
| Topic name length | 80 characters | How granular your topic keys can be |
| Subscriptions per server | 20 + 8 × players | Whether per-player or per-guild topics are viable |
| Messages sent per server | 600 + 240 × players, per minute | Broadcast frequency and retry-storm headroom |
| Messages received per universe | 720 + 240 × players, per minute | Total cross-server chatter at full population |
The important asymmetry is that the send budget is scoped to a single game server while the receive budget is scoped to the whole universe. That is where the interesting arithmetic lives.
Consider a universe running 200 servers of 30 players each, or 6,000 concurrent players. A single broadcast costs one receive per subscribed server, so that one publish spends 200 units of the universe-wide receive budget rather than one.
Multiply that out and a chatty subsystem gets expensive quickly. Ten broadcasts per minute across 200 servers is 2,000 receives per minute before a single player has done anything unusual.
How large can a MessagingService payload be?
A MessagingService payload is capped at 1 KB after serialization, and topic names at 80 characters. Send an event ID and a version number so each server can pull the full details from its own store.
What happens when you exceed the send quota?
Send quota scales with population, roughly 600 plus 240 per player each minute, per server. Publish on state changes rather than on a heartbeat, and batch several updates into one message when they land in the same tick.
Because PublishAsync throws on failure, an unwrapped call inside a game loop surfaces as a broken script rather than a dropped message. Wrap it in pcall, count the failures, and expose that counter — a rising publish-failure rate is the earliest warning that your event design outgrew its budget.
Designing Topics That Do Not Burn Your Subscription Budget
Topic names cap out at 80 characters, which is enough for a namespaced key and nothing more. A convention like global:announce, shard:12:state, or guild:1042:roster keeps parsing trivial and keeps you well under the ceiling.
The real constraint is subscription count, which scales with the player population on each server. Any design that opens a topic per player or per party will hit that ceiling on a full server long before it hits a send limit.
| Topic strategy | Subscriptions per server | Best for | Fails when |
|---|---|---|---|
| Single broadcast topic | 1 | Announcements, live-event timelines, config pushes | Volume forces every server to decode traffic it does not need |
| Fixed subsystem topics | 5 to 20 | Separating trading, moderation, leaderboards, and events | Topic count creeps upward with every new feature |
| Sharded topics by region or mode | 1 to 4 | Regional events, mode-specific state, pooled matchmaking | A server needs to change shards mid-session |
| Per-entity topics | Scales with population | Small universes with very small servers | A full server exhausts its subscription budget |
For most games the honest answer is one broadcast topic plus a small fixed set of subsystem topics. Every server then decodes every broadcast message, which costs a little CPU and buys you a subscription count that does not move with population.
That said, sharding earns its keep once regions or game modes diverge. If your Roblox matchmaking layer already groups servers into pools, reuse that grouping as the topic key instead of inventing a second one.
Payload Design Under A One-Kilobyte Ceiling
One kilobyte is measured after serialization, in bytes rather than characters. A payload carrying emoji-laden display names will pass the ceiling at a fraction of the string length you expected.
The discipline that keeps you inside it is to send a pointer and a version, then let each receiving server pull the details from a store it already trusts. Here is what belongs in a well-formed cross-server payload:
- Event ID. A unique string per logical event, used for deduplication on the receiving side. Without it, a retried publish applies twice.
- Version. A monotonically increasing integer for the subsystem this message belongs to. Receivers discard anything at or below the version they have already applied.
- Type. A short enum-style string so one shared topic can carry several kinds of message. Keep it to a few characters and map it to a handler table on receipt.
- Sender. The publishing server's
game.JobId, so receivers can recognize and ignore their own echo. - Pointer. The store key holding the full state rather than the state itself. This is what keeps payload size flat no matter how large the underlying change is.
All of these together fit comfortably inside a couple hundred bytes, which leaves headroom for the one or two fields a specific event genuinely needs inline. Shortening key names is a legitimate optimization here, since using a single character instead of a word like eventType saves eight bytes on every message you will ever send.
Why does a server receive its own broadcast?
The server that calls PublishAsync also receives its own message if it subscribed to that topic. Stamp every payload with game.JobId and drop messages whose sender ID matches your own, or you will double-apply the event.
Decide once whether the publisher applies the effect locally or waits for its own echo, then enforce that decision in code. Mixing the two is the single most common source of duplicated rewards in cross-server systems.
Four Fan-Out Patterns That Survive Contact With Production
Straight Broadcast
One topic, every server subscribed, the full message inline. It is the simplest pattern and the correct one for announcements where a missed message costs nothing more than a player not seeing a banner.
The trap is using it for state. Once a broadcast carries an authoritative fact such as a price change, an unlocked door, or an inventory grant, a dropped message leaves you with a permanently divergent shard.
Ping And Pull
The publisher writes the authoritative change to a DataStore or a MemoryStore sorted map first, then broadcasts a tiny message naming the key and the new version. Receivers compare that version against what they hold and read the store only when they are behind.
This is the default worth reaching for, because it converts a delivery-guarantee problem into a latency problem. A dropped ping means a server is stale until its next reconcile pass, not that it is wrong forever.
Global Roblox leaderboards are the cleanest example of the shape: an OrderedDataStore holds the truth, and the message only tells servers to refresh their cached top ten sooner than the next scheduled poll.
Coordinator Server
One server elects itself coordinator, usually by winning a MemoryStore lock with a short time-to-live, and becomes the only publisher for a given subsystem. Every other server subscribes and never publishes on that topic.
This is how you run a global boss health bar or a shared world timer without 200 servers each trying to write the same value. Be aware that the lock TTL needs to be shorter than your tolerance for a stalled event, because a coordinator can die mid-fight with no warning.
Sharded Fan-Out
Split servers into a fixed number of buckets, give each bucket its own topic, and publish once per bucket. Fan-out cost then rises with bucket count rather than server count, and each server decodes only its own bucket traffic.
Use this when a message is genuinely relevant to a subset, such as a regional event or a staged rule change. For instance, rolling a change out to bucket zero first gives you a real canary before it reaches the whole universe.
Reconciling With DataStore State When A Message Is Dropped
Every cross-server subsystem needs two paths to the truth: the fast one through MessagingService and the slow one through a store you poll. The fast path is an optimization, and the slow path is the contract.
How do you recover from a dropped message?
Pair every broadcast with a periodic reconcile. Have each server re-read the authoritative version number every 30 to 60 seconds, so a dropped message costs you a delay instead of a permanently desynced shard.
A reconcile pass is unglamorous and cheap. Read a single version key from a DataStore or MemoryStore on an interval, compare it with what the server has applied, and pull the delta only when the two disagree.
Remember that ordering is not guaranteed either, so the comparison has to be a strict inequality rather than a did-anything-change check. A monotonically increasing integer per subsystem is enough, and timestamps invite trouble the first time two servers disagree about the clock.
Idempotency matters as much as ordering. Stamp each event with an ID, keep the last few dozen IDs per subsystem in a ring buffer, and drop repeats — the same discipline that keeps cross-server inventory trading from duplicating items when a confirmation arrives twice.
Finally, hook game:BindToClose so a shutting-down server flushes pending state to the store before it disappears. A message published by a server that dies two seconds later is a message nobody can reconstruct.
Rule of thumb: if losing a message would leave two servers permanently disagreeing about the world, MessagingService is the wrong home for that fact. Put it in MemoryStoreService or a DataStore and use the message only to shorten the time until the other servers notice.
The Live Event Timeline Pattern
The most common way to blow a messaging budget is broadcasting a countdown. A server publishing sixty seconds left, then fifty-nine, across 200 servers spends its entire minute-long budget on information every server could have computed itself.
Publish the schedule once instead. Broadcast a single start timestamp, write that same timestamp to a DataStore, and let every server compare it against os.time locally on its own heartbeat.
Servers that spin up after the broadcast read the timestamp from the store during startup, which is the same path a server takes after missing the ping entirely. As a result, one publish covers the whole event, including servers that did not exist when it was sent.
What's more, this pattern degrades gracefully. If the broadcast is dropped completely, every server still converges on the correct start time within one reconcile interval rather than running a countdown that is minutes out of sync.
Testing Cross-Server Code Without Losing A Day
Studio is not a cross-server environment. game.JobId is an empty string there, and Studio sessions do not exchange messages with live servers, so any dedupe logic keyed on JobId behaves differently in testing than in production.
How do you test MessagingService code properly?
Studio test sessions do not exchange messages with live servers, so cross-server logic needs two published test servers. Log the Sent timestamp on receipt to measure real fan-out latency instead of guessing at it.
The workable loop is to publish to a private test place, join two servers from two accounts, and drive the publish from a developer-only command. Log the difference between Sent and receipt time on every message, then keep the P50 and P95.
Those two numbers tell you whether a bug is a delivery problem or a logic problem, which is otherwise nearly impossible to distinguish after the fact. Fold them into whatever you already use for Roblox performance profiling so the data survives past the session that produced it.
Failure Modes Worth Guarding Against
Most cross-server incidents trace back to a short list of mistakes, and all of them are cheap to prevent at design time. Watch for the following:
- Retry storms. Every server retrying a failed publish on the same fixed interval will collide again on the next attempt. Add jitter to the backoff so retries spread out instead of synchronizing.
- Self-echo double-apply. The publisher receives its own message, so applying an effect both locally and on receipt grants the reward twice. Pick one path and enforce it with the sender ID.
- Subscribe latency at startup.
SubscribeAsyncyields, and a server can miss messages between spawning and completing its subscription. Always read current state from the store after the subscription resolves. - Trusting payload contents. Cross-server messages are server-authored, but the values inside them often originated as client input. Validate them with the same rigor you apply in your Roblox anti-exploit layer.
- Unbounded topic growth. A topic per party or per trade session looks harmless until subscription count starts tracking concurrent activity. Cap it or shard it before it caps you.
- Silent pcall. Wrapping
PublishAsyncinpcalland discarding the error turns a quota breach into an invisible one. Increment a counter and log the message type on every failure.
Frequently Asked Questions
How many topics can one Roblox server subscribe to?
Subscription budget scales with population, on the order of 20 plus 8 per player. A 30-player server therefore supports roughly 260 concurrent topics, which is ample until you open a topic per player or per guild.
Are MessagingService messages delivered in order?
No. Ordering is not guaranteed across servers, so two updates sent milliseconds apart can arrive reversed. Attach a monotonically increasing version number to every payload and discard anything at or below what the server already applied.
What happens if PublishAsync exceeds the rate limit?
It throws, so wrap every call in pcall and count the failures. Retry with exponential backoff plus jitter, because servers retrying on the same fixed interval will collide again on the very next attempt.
Should I use MessagingService or MemoryStoreService for shared state?
Use MemoryStoreService for the state itself, since sorted maps, queues, and hash maps give you an authoritative shared value with far higher throughput. Use MessagingService only to tell other servers that the value changed.
How do I run a timed global event across every server?
Publish the schedule once rather than each tick. Broadcast a single start timestamp, write it to a DataStore for servers that spin up later, and let every server compare it against os.time on its own heartbeat.
Where To Take This Next
If you are wiring your first global event, start by deciding which facts are allowed to live only in a message and which must live in a store. Nearly every other decision in this guide follows from that one call.
From there the neighboring pieces are worth reading in order: Roblox server architecture for how to slice servers in the first place, DataStore patterns for the durable layer underneath, and Luau scripting patterns for structuring the modules that hold it all together.


