Photon Interest Groups: Stop Paying to Send Data Nobody Needs
Photon broadcasts every event to everyone in a room by default. Interest groups and PUN's Network Culling let the server filter delivery — here's when that pays off and how it fails silently.
16 Jul 2025, 18:43 UTC

Your Photon room works fine with four players. Then you test with twenty, and everything still "works" — but bandwidth climbs, clients churn through irrelevant events, and your CCU-based bill grows faster than your player count. The default behavior in Photon Realtime and PUN is simple: RaiseEvent and PhotonView synchronization go to everyone in the room. That's a fine default for a prototype and a wasteful one for anything bigger.
The built-in fix is interest management: interest groups in the Realtime API, and PUN's higher-level Network Culling on top of it. The idea is that the server filters delivery, so a client only receives events published to groups it has explicitly joined. This post covers how the mechanism works, a concrete setup, and where it breaks down.
Broadcast is a scaling decision, not just a default
Every event you raise in a Photon room costs messages on the server side and deserialization work on every receiving client. With N players each sending position updates, naive broadcast is O(N²) in delivered messages per tick. At 8 players that's noise; at 40 players in one room it's a real cost, both in bandwidth and in client CPU spent processing updates about players 400 meters away who will never matter.
Interest groups change the delivery rule: each event is tagged with a group ID (a byte, so 0–255 in the Realtime API), and the server only forwards it to clients subscribed to that group. Group 0 is the default — everyone is in it, and events raised without a group go there. Nothing else changes: reliability and ordering options still apply per event, and the server still does the routing for you.
Two ways to use it: manual groups vs. Network Culling
Manual interest groups fit logical partitioning. Team-only chat, admin/debug events, per-match traffic when several matches share a room — anything where "who should hear this" is a rule you can express in code. In PUN 2 the relevant calls are on PhotonNetwork:
// Client joins the groups it cares about (run on each client).
// null = leave all, empty array = leave none; pass groups explicitly.
byte[] myGroups = new byte[] { 1, 7 }; // e.g. team 1, map cell 7
PhotonNetwork.SetInterestGroups(null, myGroups);
// Send an event only to group 7.
RaiseEventOptions opts = new RaiseEventOptions {
InterestGroup = 7,
Receivers = ReceiverGroup.Others
};
PhotonNetwork.RaiseEvent(MyEventCode, payload, opts, SendOptions.SendReliable);Network Culling is PUN's spatial automation of the same mechanism. You drop a CullingArea component into the scene, size its grid cells to roughly your players' visibility range, and PUN maps each player's position to a cell, subscribes them to neighboring cells' groups, and only serializes PhotonViews whose observed objects are in subscribed cells. You get area-of-interest behavior without hand-managing group arrays as players move.
A worked example: team voice-style events
Say you have a 24-player room split into three teams, and team-only ping events that should never cross teams. Assign team 0 → group 1, team 1 → group 2, team 2 → group 3. On join, each client calls SetInterestGroups with its team's group plus group 0 (for match-wide events). Ping events are raised with InterestGroup set to the sender's team group.
To verify it works, run a two-client test: put client A on team 1, client B on team 2, raise a ping from A, and confirm B's OnEvent callback never fires while A's teammate's does. Then check the Photon dashboard's message counters before and after — with three teams you should see delivered ping messages drop to roughly a third of broadcast. Don't trust the code path alone; the dashboard numbers are the ground truth for what you actually saved.
The trade-offs that bite in production
Silent failure mode. Culling bugs don't throw errors — a client that forgot to subscribe simply receives nothing. This looks exactly like packet loss or a sync bug. Make subscription state loggable and assert it after team changes or respawns.
Group changes are server round trips. If you update interest groups as players move (rather than using CullingArea), throttle the updates. Calling SetInterestGroups every frame is worse than the bandwidth you saved. Batch changes or only update when the player's cell actually changes.
Don't cull critical state. Match-end events, score finalization, ownership transfers — keep these on group 0 or a guaranteed channel. Culling decides who receives an event; it adds a failure mode where the answer is "nobody, because of a stale subscription."
Version sensitivity. Group limits and API names differ between PUN 2, Photon Realtime, and Fusion/Quantum (which use entirely different replication models). Check the changelog and API reference for your installed SDK version before relying on the 256-group figure or specific method signatures.
When to bother
Under roughly 8–16 players per room, plain broadcast is simpler and the savings are negligible — skip culling and spend the complexity budget elsewhere. Adopt interest groups when rooms get large, when you have logically separable traffic (teams, chat channels, debug events), or when dashboard message counts show broadcast dominating your bandwidth. Start with the manual group API for logical partitioning — it's a small code change with measurable payoff — and reach for Network Culling when the partitioning is genuinely spatial.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.