Funnel events: finding out at which step the player gives up, not only that they did
The dashboard says retention is low. The funnel says where. There are two event types — one-off per player and recurring — and they only work on the server, in a published game.
Produced with AI from the sources cited below, and checked by automated verification against those same sources. No person read this text before publication. Editorial Policy

The retention dashboard says D1 is low. That is a diagnosis and it is not a direction — you can spend weeks changing the game without knowing whether you are fixing the right thing.
The funnel answers the next question: at which step people stop. And the difference between knowing they stop and knowing where is the difference between guessing and working.
The problem
Funnel events track the player's progress through the game's important stages. The documentation lists three questions they answer, and each corresponds to a type of funnel:
- First session: where does the player give up when starting out?
- Progression: where do they stop advancing?
- Shop: where do they abandon the purchase?
With the game recording those events, the analytics dashboard's Funnel page in the Creator Hub unlocks. And you can add tabs for up to ten funnels — which is plenty, and still a limit worth keeping in mind before instrumenting everything.
There is a platform restriction that changes the workflow, and it needs saying early: the events can only be sent from the server and in a published game.
In Studio, nothing is sent. That is not an error or a missing setting — it is the behaviour. Anyone who tests the instrumentation in Studio and goes looking for the data in the dashboard loses an afternoon before finding out.
Step by step
1. Choose the funnels. The first step is identifying the game's most important funnels and dividing them into steps. And the documentation gives a priority recommendation: the first session flow is an excellent place to start, because it is probably where most players are lost.
2. Divide into steps. Steps usually correspond to an action or an achievement. The official example is an RPG: one step for joining the game, another for choosing a class, another for leaving the starting room, and so on.
Notice the format: they are things the player does, not time elapsed or a screen opened. A step that does not correspond to an action says nothing when the funnel shows the drop there.
3. Record after the step. The call happens after the player completes the step. And here is the rule the documentation highlights above any technical detail: the most important thing is being consistent about when you call the methods and using names that are easy to interpret.
Both halves of that sentence matter. Consistency of moment — always after, never before — because one step recorded on intention and another on completion produce a funnel mixing two things. And readable names because the dashboard shows the name, and "step3" tells nobody, three months from now, what was being measured.
4. Choose the type. There are two, and the difference is about repetition:
- A one-off funnel tracks a conversion that happens only once per player. The common example is the first session.
- A recurring funnel tracks a conversion that happens several times per player. The common example is the shop.
5. Use a session identifier where it is needed. In a recurring funnel, the identifier distinguishes different sessions by the same player — the case cited is the player opening the shop several times in the same play session.
In a one-off funnel, it is not necessary, precisely because the funnel occurs once per player.
And for the shop case, the recommendation is specific: use a GUID as the identifier.
A detail about the step number. It is not just a label: it is the order in the graph. Changing the numbering of an already published funnel rewrites how the dashboard builds the sequence, and the earlier history has to live alongside the new numbering.
The practical consequence is that it is worth settling the list before publishing the instrumentation, and treating it as stable afterwards. Adding a step at the end is cheap; inserting one in the middle renumbers everything after it and muddles comparison with the data you already have.
The artifact
The module below implements both types in one place, and its most important design decision is not technical: the steps are declared in a table, with number and name together.
The reason is the documentation's golden rule. A name typed by hand at five call sites becomes five different names — one with a capital, another plural, another with an extra space. And the dashboard, which groups by name, shows five one-step funnels instead of one five-step funnel.
Beyond that, it does two things the documentation does not ask for and that save the data: it ignores a repeated step and refuses an out-of-order step. A funnel with a skipped step is unreadable, and a skipped step is almost always a call bug, not player behaviour.
And it returns early in Studio, without trying to send — which avoids noise in the output during development.
It is worth noting the difference in purpose between the two types, because it decides which to use when the case is not obvious. The question is not "how many times does the player do this?", it is "how many times can this CONVERT?". Choosing a class happens once and converts once: a one-off funnel. Opening the shop happens many times and can convert each time: a recurring funnel. A tutorial the player can redo is still one-off, because the conversion that matters is the first.
Common mistakes
Testing in Studio. The first and most frustrating. The events only leave from the server and in a published game. The instrumentation looks like it works — the code runs, it raises no error — and the dashboard stays empty. The cause is not your code.
Recording before the action. Calling the event when the player opens the class selection screen, instead of when they choose, turns the step into "saw the screen". The funnel still makes visual sense and measures something else — and the drop you will see is not the drop that exists.
Inconsistent names. Already described, and worth repeating because it is silent: nothing breaks, the dashboard simply shows fragmented funnels nobody understands.
Forgetting the session identifier in a recurring funnel. Without it, two shop visits by the same player collapse into one. The result is a funnel that looks too good: the drop-off rate vanishes, because the second visit "completes" what the first left halfway.
Using a session identifier in a one-off funnel. The reverse mistake, and the documentation is explicit that it is not necessary. It breaks nothing and adds a variable to a funnel that does not have that dimension.
Over-instrumenting. With the ten-funnel limit in the dashboard, the temptation to measure everything produces ten tabs nobody opens. One well-made funnel of the first minute is worth more than ten funnels of systems almost nobody uses.
Too many steps. A fifteen-step funnel is a graph that goes down and does not say where. The practical rule: each step has to be something you would know how to fix if the drop were there. If you do not, it is not a step — it is noise between two real steps.
Expecting the data to appear immediately. The analytics dashboard is not real time, and the first day of instrumentation usually yields an empty or shallow graph. That leads people to change an implementation that was right. Publish, wait for a full day's normal volume, and only then judge.
Next step
Today's exercise needs no code: write down, one line each, the steps of your game's first minute. From the click on play to the moment the player does, for the first time, the thing the game exists for them to do.
If the list has more than six steps, your opening is long — and that is already a finding, before any instrumentation. If it has fewer than three, there are probably implicit steps you know too well to see.
That list becomes, directly, the artifact's table. And when the data starts arriving, it is what lets you read the graph: the biggest drop between two consecutive steps is the place to work, and you already know what happens there because you wrote it.
After the first session funnel is working, the next one with the best return is usually the shop — because it is recurring, it measures money, and the drop in it is usually at a step that has nothing to do with price.
Sources
- Funnel events | Documentation - Roblox Creator Hub — checked on
Ready-to-use artifact
-- ServerScriptService: both funnel types, in one place.
--
-- THE GOLDEN RULE from the documentation: be CONSISTENT about when you
-- call, and use names that are EASY TO INTERPRET. That is why the steps
-- are declared in a table, rather than typed loose at each call site --
-- a name typed by hand in five places becomes five different names, and
-- the dashboard shows five one-step funnels.
--
-- PLATFORM LIMIT: the events are only sent FROM THE SERVER and in a
-- PUBLISHED game. In Studio nothing is sent -- and that is not an error,
-- it is the behaviour. Do not spend an afternoon looking for the data in
-- the dashboard after testing in Studio.
local AnalyticsService = game:GetService("AnalyticsService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local Funnel = {}
-- ── FIRST SESSION FUNNEL (once per player) ──────────────────────────
-- Number and name together, in order. Changing the order here rewrites
-- the whole funnel in the dashboard, so it is worth settling it before
-- publishing.
local FIRST_SESSION = {
{ n = 1, name = "Joined the game" },
{ n = 2, name = "Chose a class" },
{ n = 3, name = "Left the starting room" },
{ n = 4, name = "First combat" },
}
local byName = {}
for _, p in FIRST_SESSION do
byName[p.name] = p.n
end
-- idempotent: repeating the same step does not count twice, and
-- skipping a step is not accepted. A funnel with an out-of-order step
-- is unreadable.
local reached = {}
function Funnel.firstSession(player, stepName)
local n = byName[stepName]
if not n then
warn("unknown step in the funnel: " .. tostring(stepName))
return
end
reached[player] = reached[player] or 0
if n <= reached[player] then
return
end
if n ~= reached[player] + 1 then
warn("funnel out of order: " .. stepName .. " after " .. reached[player])
return
end
reached[player] = n
if RunService:IsStudio() then
return -- nothing is sent in Studio; this avoids noise in the output
end
pcall(function()
AnalyticsService:LogOnboardingFunnelStepEvent(player, n, stepName)
end)
end
-- ── RECURRING FUNNEL (several times per player) ─────────────────────
-- Here the session identifier is mandatory: without it, two shop visits
-- by the same player collapse into one, and the drop-off rate vanishes.
local SHOP = "ArmoryCheckout"
local SHOP_STEPS = {
{ n = 1, name = "Opened the shop" },
{ n = 2, name = "Viewed the item" },
{ n = 3, name = "Added to the cart" },
{ n = 4, name = "Confirmed the purchase" },
}
local shopByName = {}
for _, p in SHOP_STEPS do
shopByName[p.name] = p.n
end
-- Call this on OPENING the shop: each opening is a new funnel session.
function Funnel.newShopSession()
return HttpService:GenerateGUID()
end
function Funnel.shop(player, sessionId, stepName)
local n = shopByName[stepName]
if not n or not sessionId then
warn("invalid step or session in the shop funnel")
return
end
if RunService:IsStudio() then
return
end
pcall(function()
AnalyticsService:LogFunnelStepEvent(player, SHOP, sessionId, n, stepName)
end)
end
game:GetService("Players").PlayerRemoving:Connect(function(player)
reached[player] = nil
end)
return Funnel


