I believe there is a race condition with Humanoid.Health where setting a Humanoid’s health to 0 does not necessarily guarantee that the Humanoid dies if its health is increased again immediately afterwards.
Reproduction
This can be reproduced with:
local humanoid = workspace.Dummy.Humanoid
humanoid.Health -= 1000000000
humanoid.Health += 100
Despite the first line reducing the Humanoid’s health to 0, the Humanoid can remain alive and end up with positive health after the second line.
A similar issue can happen naturally with Roblox’s default character health regeneration script.
The default regeneration code (a script which spawns under player characters called "Health"):
-- Gradually regenerates the Humanoid's Health over time.
local REGEN_RATE = 1/100 -- Regenerate this fraction of MaxHealth per second.
local REGEN_STEP = 1 -- Wait this long between each regeneration step.
--------------------------------------------------------------------------------
local Character = script.Parent
local Humanoid = Character:WaitForChild'Humanoid'
--------------------------------------------------------------------------------
while true do
while Humanoid.Health < Humanoid.MaxHealth do
local dt = wait(REGEN_STEP)
local dh = dt*REGEN_RATE*Humanoid.MaxHealth
Humanoid.Health = math.min(Humanoid.Health + dh, Humanoid.MaxHealth)
end
Humanoid.HealthChanged:Wait()
end
Because the script yields during wait(REGEN_STEP), the following sequence is possible:
- The regeneration loop sees that the Humanoid is below
MaxHealth. - The regeneration script begins waiting.
- Another server script deals lethal damage, reducing
Healthto0. - The regeneration thread resumes before the Humanoid has fully transitioned into the dead state.
- The regeneration script increases
Healthabove0. - The Humanoid survives.
Expected behaviour
Once:
humanoid.Health = 0
has occurred, I would expect that Humanoid to be committed to dying and for subsequent health changes to be ignored (or at least not cancel the death).
Actual behaviour
There appears to be a short period between Health reaching 0 and the Humanoid actually entering its dead state where another script can increase its health again and prevent the death.
This can make server-side damage systems nondeterministic if healing/regeneration happens at approximately the same time as lethal damage.
Workaround
For now, developers can prevent this by ensuring that no healing code modifies a Humanoid once its health has reached 0.
For example, the default health regeneration behaviour can be made safer by checking again after the yield:
local dt = task.wait(REGEN_STEP)
if humanoid.Health <= 0 then
return
end
local dh = dt * REGEN_RATE * humanoid.MaxHealth
humanoid.Health += dh
However, I think the underlying Humanoid behaviour should ideally be fixed at the engine level, since developers generally expect assigning Health = 0 to reliably kill a Humanoid regardless of another health modification occurring immediately afterwards.
Why this matters
This can affect combat systems, regeneration systems, damage-over-time, healing abilities, environmental damage, and any other systems where multiple server scripts may modify Humanoid.Health.
The particularly surprising part is that simply reaching 0 health does not appear to guarantee death if health is restored quickly enough afterwards.
Additionally, the default health script is severely outdated and relies on deprecated features like wait() instead of task.wait(). Replacing it with a modern version will ensure long-term stability and also fix the health regeneration bugs mentioned above.