Blog/Roblox/🎮Game Guides

Roblox CollectionService Guide: Tag Once, Script Everything

If your Explorer has forty copies of the same killbrick script, you don't have a game — you have forty bugs waiting to disagree with each other. CollectionService fixes that: tag the objects, write one handler, done. Here's the exact pattern, the attribute layer that makes each tagged object configurable, the cleanup step most tutorials skip, and the replication behavior that quietly eats client-side tags.

Published July 28, 2026·13 min read·By Mythras
Roblox Studio in its dark theme showing the 3D viewport, the Explorer tree, and the Properties window where the Tags and Attributes sections used by CollectionService live.

Open the Explorer on any obby that's been in development for more than a month and you'll find it: a Script called Kill sitting inside a part, and then thirty-nine more copies of it inside thirty-nine other parts. Someone fixed a bug in seven of them. Someone else changed the damage value in four. Nobody remembers which. That project doesn't have forty killbricks — it has forty slightly different opinions about what a killbrick is.

The fix is a service most people skim past in the API reference. Roblox's own documentation puts it about as bluntly as documentation ever does: "If you find yourself adding the same script to many different instances, a script that uses CollectionService may be better." That's not a style suggestion. It's the difference between a codebase you can change and one you can only add to.

Roblox Studio in its dark theme showing the 3D viewport, the Explorer tree, and the Properties window where the Tags and Attributes sections live.

The forty-scripts problem

Copy-pasted scripts fail in a specific, predictable way, and it isn't "the game runs slow." It's that every copy is a separate source of truth. Change one, and you've created a divergence you can't see from the Explorer, because two Scripts named Kill look identical in the tree whether or not their contents match.

You've probably already solved the code-duplication half of this with ModuleScripts — one module, required from everywhere. Good. But that only dedupes the logic. You still have forty Scripts whose entire job is to require the module and point it at their parent. Tags kill that last layer: one script, zero copies, any number of objects.

What a tag actually is

A tag is a string attached to an instance. That's genuinely the whole data model. Per the official docs, "Tags are sets of strings applied to instances that replicate from the server to the client. They are also serialized when places are saved."

Three consequences fall straight out of that sentence, and all three matter:

  • Tags survive saves. Tag a part in Studio, save the place, and the tag is still there next session. You are editing data, not writing setup code.
  • Tags replicate server to client. A part tagged on the server is tagged on every client, so a client-side visual handler can find it without a RemoteEvent handshake.
  • Tags are just strings. There's no tag object to manage, no registry to keep in sync, no schema. A tag exists because at least one instance has it, and stops existing when the last instance loses it.

CollectionService is the service that manages those collections. The API is small enough to memorize in one sitting:

local CollectionService = game:GetService("CollectionService")

CollectionService:AddTag(instance, "Killbrick")     -- apply
CollectionService:RemoveTag(instance, "Killbrick")  -- remove
CollectionService:HasTag(instance, "Killbrick")     -- boolean
CollectionService:GetTags(instance)                 -- array of that instance's tags
CollectionService:GetTagged("Killbrick")            -- array of instances with the tag
CollectionService:GetAllTags()                      -- every tag in the experience

Modern Luau also lets you call four of those directly on the instance, which reads better in most code:

part:AddTag("Killbrick")
part:RemoveTag("Killbrick")
print(part:HasTag("Killbrick"))  --> true / false
print(part:GetTags())            --> { "Killbrick" }

Two things you'll see in old tutorials and should ignore: CollectionService:GetCollection() and the ItemAdded / ItemRemoved events. All three are deprecated, and GetCollection only ever worked for five obscure classes — Configuration, CustomEvent, CustomEventReceiver, Dialog, and VehicleSeat. GetTagged supersedes it.

Tagging in Studio: the two-click workflow

You don't need a plugin, and you don't need to tag things in code. Tags live in the Properties window — the one you open from Studio's Window menu or the Home tab toolbar.

Select an instance, scroll to the bottom of Properties, find the Tags section, and click the + button.

The Tags section at the bottom of the Roblox Studio Properties window, with the plus button used to add a new tag to the selected instance.

A popup appears. Type a new tag name, or pick one you've already used on any other instance from the list underneath the input — that list is what stops your project from ending up with Killbrick, killbrick, and KillBrick as three unrelated tags. Removing a tag is the × icon next to it.

The tag popup in the Roblox Studio Properties window, showing a text field for a new tag name and a list of existing tags to choose from.

The part people miss: multi-select works. Rubber-band forty parts in the viewport, hit + once, and all forty are tagged. That's the moment the workflow clicks — you just did in four seconds what copy-pasting a script does in four minutes, badly.

Pro move while you're in there: <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>P</kbd> (or <kbd></kbd><kbd>Shift</kbd><kbd>P</kbd> on Mac) jumps focus straight to the Properties filter box, which is how you stop scrolling past twelve collapsed sections every time you want the Tags panel.

The pattern that replaces forty scripts

Here's the whole thing. One Script in ServerScriptService, handling every killbrick in the place — existing, future, and streamed-in.

local CollectionService = game:GetService("CollectionService")

local TAG = "Killbrick"
local connections = {}

local function onAdded(part)
	if not part:IsA("BasePart") then
		return
	end

	local damage = part:GetAttribute("Damage") or 100

	connections[part] = part.Touched:Connect(function(hit)
		local humanoid = hit.Parent and hit.Parent:FindFirstChildWhichIsA("Humanoid")
		if humanoid then
			humanoid:TakeDamage(damage)
		end
	end)
end

local function onRemoved(part)
	local connection = connections[part]
	if connection then
		connection:Disconnect()
		connections[part] = nil
	end
end

-- Everything that already has the tag
for _, part in CollectionService:GetTagged(TAG) do
	onAdded(part)
end

-- Everything that gets it later
CollectionService:GetInstanceAddedSignal(TAG):Connect(onAdded)
CollectionService:GetInstanceRemovedSignal(TAG):Connect(onRemoved)

That's the canonical shape, and it's worth understanding why it has three parts instead of one.

Why GetTagged alone is a bug

GetTagged returns instances that have the tag right now and are descendants of the DataModel. It is a snapshot, not a subscription. Loop over it once at startup and every part that spawns later — every regenerated obstacle course, every dropped weapon, every enemy your spawner clones in — is invisible to your handler.

The docs are explicit about the fix: "If you want to detect all instances with a tag, both present and future, use this method to iterate over instances while also making a connection to a signal returned by GetInstanceAddedSignal." Snapshot plus subscription. Both, always.

Two more GetTagged behaviors that bite in practice:

  • No ordering guarantee. The docs say so directly. If your logic depends on "the first checkpoint," sort the results yourself — don't trust index 1.
  • Unparented instances don't count. An instance can carry the tag and still not be returned if it isn't a descendant of the DataModel, for example while it's sitting with Parent = nil mid-swap.

And a detail that saves you a subtle class of bug: GetInstanceAddedSignal fires under two conditions, not one. It fires when the tag is applied via AddTag, and it fires when an already-tagged instance becomes a descendant of the DataModel — i.e. when you parent a pre-tagged clone into the Workspace. Which means you can tag a template once, inside ServerStorage, and every clone you parent in wires itself up automatically. That's the actual superpower here. Your spawner code shrinks to template:Clone().Parent = workspace.

(Repeated calls with the same tag hand back the same signal object, so calling GetInstanceAddedSignal("Killbrick") in three modules doesn't create three signals. It's cheap.)

Attributes: the other half of the pattern

Tags answer what kind of thing is this. They don't answer how much damage. That's what attributes are for, and using them together is what turns a tag system into an actual component system.

Attributes are custom properties you define on any instance. They're created in the Properties window the same way tags are — scroll to the Attributes section, click +, give it a name and a type, hit Save — and they're saved with your place and replicated to clients.

The Attributes section of the Roblox Studio Properties window showing a custom attribute with its name, type and editable value.

So the killbrick that takes 100 HP and the spike pit that takes 25 are the same tag with different Damage attributes. No second script. No if part.Name == "SpikePit" branch. In code:

part:SetAttribute("Damage", 25)
local damage = part:GetAttribute("Damage")   --> 25
local all = part:GetAttributes()             --> dictionary of every attribute
part:SetAttribute("Damage", nil)             -- setting nil DELETES the attribute

Set an attribute that doesn't exist yet and it gets created. Set it to nil and it's gone — there is no separate delete call.

Now make it live-tunable. Because attributes replicate and fire change signals, you can retune a hazard in a running playtest and watch it apply without a restart:

local function onAdded(part)
	local damage = part:GetAttribute("Damage") or 100

	part:GetAttributeChangedSignal("Damage"):Connect(function()
		damage = part:GetAttribute("Damage") or 100
	end)

	connections[part] = part.Touched:Connect(function(hit)
		local humanoid = hit.Parent and hit.Parent:FindFirstChildWhichIsA("Humanoid")
		if humanoid then
			humanoid:TakeDamage(damage)
		end
	end)
end

If you want to react to any attribute changing rather than one specific one, Instance.AttributeChanged fires with the changed attribute's name as its argument. GetAttributeChangedSignal passes nothing, which is why it pairs so cleanly with anonymous functions.

The attribute rules that throw errors

These are documented limits, and hitting them at 2am is annoying enough that they're worth memorizing:

  • Names are alphanumeric, plus periods, hyphens, slashes, and underscores. No spaces. No @, ~, ! or friends.
  • 100 characters max.
  • Names can't start with RBX — that prefix is reserved for Roblox core scripts.
  • Only supported types are allowed, and setting an unsupported one throws an error rather than silently failing. The supported set is: string, boolean, number, UDim, UDim2, BrickColor, Color3, Vector2, Vector3, CFrame, NumberSequence, ColorSequence, NumberRange, Rect, and Font.

Notice what's not on that list: tables, Instances, and Enums. If you want a tagged part to point at another instance, an attribute won't do it — use an ObjectValue child, or store a path string and resolve it.

Cleanup, or you just wrote a memory leak

This is the step tutorials skip, and it's the reason some tag systems quietly eat memory over a two-hour server session.

When you wire up a tagged instance, you usually allocate something — an event connection, a table entry, a tween, a task loop. Roblox's own docs flag it in the AddTag warnings: to prevent memory leaks, clean those up when they're no longer needed, either when you call RemoveTag, when you call Destroy, or in a handler connected to GetInstanceRemovedSignal.

The connections table in the pattern above is exactly that. Note that it's keyed by the instance, which makes teardown a one-liner. And note when the removed signal fires: on RemoveTag, and when the instance stops being a descendant of the DataModel — which is what Destroy does. So one onRemoved function covers both the "this part is no longer a killbrick" case and the "this part no longer exists" case.

If you're chasing a leak that this doesn't explain, the wider memory leaks and optimization guide covers the other usual suspects.

The replication trap nobody warns you about

Here's the one that'll cost you an afternoon if you don't know it. Straight from the CollectionService docs:

When tags replicate, all tags on an instance replicate at the same time. Therefore, if you set a tag on an instance from the client then add/remove a different tag on the same instance from the server, the client's local tags on the instance are overwritten.

Read that twice. Tags don't replicate individually — they replicate as a set. So a LocalScript that adds a client-only marker tag like Highlighted to a part is living on borrowed time. The instant the server touches any tag on that same part, the whole set gets overwritten and your client-side tag is gone. No error, no warning, just a handler that stops firing.

It gets sharper in a place with StreamingEnabled. The docs note that instances unload as they leave the client's streamed area, and when they re-enter, "properties and tags will be re-synchronized from the server," which "can cause changes made by LocalScripts to be overwritten/removed." Practically, that means two things for client code in a streaming place: your local-only tags are unreliable, and because streaming in and out changes DataModel descendancy, expect the added and removed signals to fire more than once for the same part over a session. Design your onAdded to be idempotent — the connections[part] guard above is the cheap version of that.

The rule I'd hand a junior dev: tag on the server, read on the client. If you need client-only state, use a plain Lua table keyed by instance, not a tag. Server-authoritative logic belongs behind RemoteEvents anyway.

A tag audit you can run in ten seconds

GetAllTags returns every tag in the experience, which makes this a genuinely useful thing to paste into the Studio command bar before you ship:

local CollectionService = game:GetService("CollectionService")

for _, tag in CollectionService:GetAllTags() do
	print(tag, "->", #CollectionService:GetTagged(tag))
end

You're looking for three tells. A tag with a count of zero is dead — nothing has it, and whatever script listens for it is doing nothing. A tag that shows up twice with different casing is the duplicate-tag bug, and it's the single most common way tag systems break silently. And a count that's way higher than you expected usually means a template got tagged and then cloned a few hundred times.

When not to use tags

Tags are a great hammer. Not everything is a nail.

  • Singletons. If there's exactly one lobby spawn or one boss arena, a direct reference or a Folder in ReplicatedStorage is clearer than a tag with one member. Tags are for collections.
  • Order-dependent sets. Checkpoints, waypoints, wave order — GetTagged explicitly gives you no ordering guarantee, so you'd be sorting on an attribute anyway. If order is the primary structure, a numbered folder is honest about that.
  • Anything you need to iterate every frame. GetTagged builds and returns an array each call. Call it once, cache the result, and maintain the cache from the added/removed signals — don't call it inside a RunService loop.
  • Data that belongs in a module. Tags describe instances in the world. Balance tables, loot tables, and shop prices belong in a server-side ModuleScript where exploiters can't read them.

The line I'd draw: if you can finish the sentence "everything tagged X should behave like Y," it's a tag. If you can't, it's probably just a variable.

Quick Action Checklist

  • Open Properties → Tags, multi-select your objects, and tag them all in one click instead of copy-pasting a Script.
  • Reuse existing tag names from the popup list. Casing typos create silent duplicate tags.
  • Every handler is three parts: loop GetTagged, connect GetInstanceAddedSignal, connect GetInstanceRemovedSignal. Never just the first one.
  • Store per-instance config in attributes (Damage, Cooldown, SpinSpeed) so one tag covers many variants.
  • Attribute names: alphanumeric plus . - / _, no spaces, under 100 characters, never starting with RBX.
  • Delete an attribute by setting it to nil — there's no separate delete method.
  • Keep a connections table keyed by instance and disconnect in the removed handler, or you're leaking.
  • Tag on the server. Client-added tags get wiped whenever the server changes any tag on that instance.
  • In a StreamingEnabled place, assume added/removed fire repeatedly per part and make onAdded idempotent.
  • Before shipping, run the GetAllTags audit in the command bar and delete anything with a count of zero.

Do this once on a real project and you'll never go back. The payoff isn't that the code is shorter — it's that "change how every hazard in the game works" becomes a five-line edit instead of an archaeology expedition through the Explorer.

Frequently Asked Questions

CollectionService is the Roblox service that manages groups of instances using tags. Tags are sets of strings applied to instances, they replicate from the server to the client, and they are serialized when a place is saved. The point of it is to stop you duplicating the same script into dozens of objects: instead of forty copies of a killbrick script, you tag forty parts and write one script that handles anything with that tag. Roblox's own documentation states that if you find yourself adding the same script to many different instances, a script using CollectionService may be better.

Keep Reading

Sources & Further Reading

Related Guides

The Roblox Studio Animation Editor window with its sections labeled: the media playback controls, the track list of rig body parts on the left, and the keyframe timeline running across the right.
🎮Game GuidesJul 25, 2026·12 min read

Roblox Animation Guide: Make and Play Custom Character Animations

Custom animations are the difference between a Roblox game that feels made and one that feels like a baseplate with scripts. Here's the whole pipeline: rig, keyframes, easing, publishing, and the Animator code that plays it — plus the priority rule that silently eats your attack animation.

Read article
The Roblox Studio 3D viewport where camera scripts are tested, showing a built scene with the Explorer and Properties panels used to inspect the Camera object.
🎮Game GuidesJul 22, 2026·11 min read

Roblox Camera Manipulation Guide: Scriptable Cameras, CFrame, and FOV

A custom camera is the difference between a game that feels like a template and one that feels made. Here's how the Roblox camera actually works — CameraType.Scriptable, CurrentCamera.CFrame, FieldOfView, and the render-step loop that keeps it from stuttering — plus the traps that make camera scripts fight the default controller.

Read article
The Roblox Studio interface where raycasting scripts are written, showing the 3D viewport with a built scene, the Explorer tree, and the Properties panel.
🎮Game GuidesJul 21, 2026·11 min read

Roblox Raycasting Guide: Hitscan Guns, Ground Checks, and RaycastParams

Raycasting is the single most reusable tool in Roblox scripting. Guns, ground checks, lasers, interaction prompts, line-of-sight AI, wall detection — all the same function. Here's how workspace:Raycast actually works, how to filter it so your gun stops shooting the player holding it, and the mistakes that make rays silently miss.

Read article
The Roblox Studio interface where ModuleScripts are written, showing the 3D viewport, the Explorer tree with services like ReplicatedStorage, and the script editor used for Luau code.
🎮Game GuidesJul 20, 2026·12 min read

Roblox ModuleScripts Guide: Stop Copy-Pasting Code

The moment you paste the same block of code into a third script, you've outgrown Scripts and LocalScripts. ModuleScripts are how real Roblox games stay maintainable — one source of truth, required from everywhere. Here's exactly how require() works, where modules live, the cache gotcha that confuses everyone, and the OOP pattern that powers most serious codebases.

Read article
Roblox experiences carousel showing game tiles including Driving Empire, Adopt Me, Field Trip Z, and DOORS fanned around a featured racing game.
🏆Tier ListsMay 29, 2026·11 min read

Best Roblox Games to Play in 2026

Roblox's front page is engagement bait. This is the filtered version: the games with real, sustained player counts and actual staying power, sorted by what you're in the mood for.

Read article
Roblox in-game Buy Item dialog showing an item priced in Robux with a subscription discount applied to the purchase.
🎮Game GuidesMay 29, 2026·11 min read

How to Get Robux Safely (Legit Ways + Scams to Avoid)

There is no free Robux generator. There never was. Here are the actual legit ways to get Robux without overpaying, the earning methods that really work, and the scams that exist purely to steal your account.

Read article