How to add sprinting on Shift in Roblox Studio
Hold Shift to sprint, with a smooth speed ramp and a camera FOV kick, plus a stamina bar. Explains why the client can set its own WalkSpeed and what that means if you care about exploiters.
Written 11 August 2026 · about 9 minutes · every script here was written against Roblox Studio as it shipped that week
The short version
Sprinting is one property — Humanoid.WalkSpeed — and everything that makes it feel good is what you do around it: how fast it ramps, whether the camera reacts, and whether it costs anything. The bare version is fifteen lines and takes a minute. The rest of this page is the difference between that and something that feels like a game.
-- StarterPlayer > StarterPlayerScripts > SprintClient (a LocalScript)local Players = game:GetService("Players")local UserInputService = game:GetService("UserInputService")local ContextActionService = game:GetService("ContextActionService")local TweenService = game:GetService("TweenService")local WALK_SPEED = 16 -- Roblox's defaultlocal SPRINT_SPEED = 26local RAMP_TIME = 0.25 -- seconds to reach full speed. 0 = instant, and worse.local player = Players.LocalPlayerlocal humanoidlocal speedTweenlocal function setSpeed(target)if not humanoid or humanoid.Health <= 0 then return endif speedTween then speedTween:Cancel() endspeedTween = TweenService:Create(humanoid,TweenInfo.new(RAMP_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),{ WalkSpeed = target })speedTween:Play()endlocal function handleSprint(_actionName, inputState)if inputState == Enum.UserInputState.Begin thensetSpeed(SPRINT_SPEED)elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel thensetSpeed(WALK_SPEED)endreturn Enum.ContextActionResult.Passendlocal function onCharacter(character)humanoid = character:WaitForChild("Humanoid")humanoid.WalkSpeed = WALK_SPEEDendif player.Character then onCharacter(player.Character) endplayer.CharacterAdded:Connect(onCharacter)-- ContextActionService rather than UserInputService.InputBegan, on purpose.-- It gives you the mobile button for free (the last argument), it handles the-- case where the player alt-tabs mid-sprint by firing Cancel, and unbinding is-- one call instead of tracking two connections.ContextActionService:BindAction("Sprint", handleSprint, true, Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift)ContextActionService:SetTitle("Sprint", "Run")
Why ContextActionService instead of UserInputService
Nearly every sprint tutorial uses UserInputService.InputBegan. It works. It also leaves you writing three things by hand that you get free here.
- Mobile. The
truein theBindActioncall creates a touch button on phones. Without it, no phone player can sprint, and phones are most of Roblox. - Alt-tab. If the window loses focus mid-sprint, ContextActionService fires
Cancel. With raw input events the key-up never arrives and the player comes back permanently sprinting. - Turning it off. A cutscene, a menu, a no-sprint zone:
UnbindAction("Sprint")and it is gone, mobile button included.
Returning Enum.ContextActionResult.Pass matters too — it lets Shift keep doing whatever else it does, notably shift-lock, instead of swallowing the key.
Ramp the speed, do not snap it
Setting WalkSpeed = 26 directly is instant, and instant is the tell. Real acceleration is what your eye reads as weight. A quarter-second Quad-out tween is barely perceptible as a tween and completely changes how the character feels to control.
Cancel the previous tween or they fight
Tap Shift quickly a few times without the speedTween:Cancel() line and you stack overlapping tweens on the same property. The character ends up at whichever one finishes last, which is not necessarily the one matching the key you are holding — so the player releases Shift and keeps sprinting. It is intermittent, it depends on timing, and it is one line to prevent.
The camera is doing half the work
Widening the field of view while sprinting is the oldest trick in first-person games and it costs eight lines. Speed is relative and the player has nothing to compare against; pulling the FOV out makes the edges of the screen move faster, which reads as acceleration even though the number barely moved.
-- Optional but it is most of the effect. Add near the top:local camera = workspace.CurrentCameralocal FOV_WALK = 70 -- Roblox's defaultlocal FOV_SPRINT = 78local fovTweenlocal function setFov(target)if fovTween then fovTween:Cancel() endfovTween = TweenService:Create(camera,TweenInfo.new(RAMP_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),{ FieldOfView = target })fovTween:Play()end-- then call setFov(FOV_SPRINT) / setFov(FOV_WALK) alongside each setSpeed call.
Keep it subtle. 70 to 78 is a nudge. Going to 90 induces motion sickness in a fraction of players and is one of the more common reasons people quietly stop playing a game.
Stamina
Unlimited sprint means nobody ever walks, which means you built one movement speed with extra steps. Stamina is what makes the choice mean something.
-- Stamina: sprinting becomes a resource instead of a toggle.local RunService = game:GetService("RunService")local MAX_STAMINA = 5 -- seconds of sprintlocal DRAIN_RATE = 1 -- per second while sprintinglocal REGEN_RATE = 0.6 -- per second while notlocal REGEN_DELAY = 1 -- seconds after you stop before it refillslocal MIN_TO_START = 0.5 -- stops the 0.05-second stutter sprintlocal stamina = MAX_STAMINAlocal sprintHeld = falselocal sprinting = falselocal lastDrain = 0local function stopSprint()sprinting = falsesetSpeed(WALK_SPEED)setFov(FOV_WALK)endlocal function startSprint()if stamina < MIN_TO_START then return endsprinting = truesetSpeed(SPRINT_SPEED)setFov(FOV_SPRINT)endRunService.Heartbeat:Connect(function(deltaTime)if sprinting then-- Only drain while actually moving. Holding Shift standing still-- costing stamina is the single most common complaint about this-- mechanic, and the fix is one condition.local moving = humanoid and humanoid.MoveDirection.Magnitude > 0if moving thenstamina -= DRAIN_RATE * deltaTimelastDrain = os.clock()if stamina <= 0 thenstamina = 0stopSprint()endendelseif os.clock() - lastDrain > REGEN_DELAY thenstamina = math.min(MAX_STAMINA, stamina + REGEN_RATE * deltaTime)-- Resume automatically if the key is still down and you have recovered.if sprintHeld and stamina >= MIN_TO_START thenstartSprint()endendend)-- and handleSprint becomes:-- Begin -> sprintHeld = true; startSprint()-- End -> sprintHeld = false; stopSprint()
Three details in there are the difference between stamina people tolerate and stamina people hate. Only draining while actually moving, so holding Shift while standing still is free. A regen delay, so the bar does not visibly yo-yo. And MIN_TO_START, which stops the player re-triggering sprint with 0.05 seconds of stamina and stuttering in and out of it every frame.
A bar to show it
-- StarterGui > StaminaGui (ScreenGui) > Bar (Frame) > Fill (Frame)-- Fill.Size = UDim2.fromScale(1, 1), AnchorPoint (0, 0.5), Position (0, 0.5)local fill = script.Parent.Bar.Fill-- In the Heartbeat loop above:fill.Size = UDim2.fromScale(stamina / MAX_STAMINA, 1)fill.BackgroundColor3 = stamina < MIN_TO_STARTand Color3.fromRGB(220, 90, 90)or Color3.fromRGB(120, 200, 140)-- Hide it when it is full and nothing is happening, so the HUD is not-- permanently occupied by a bar that is permanently at 100%.script.Parent.Bar.Visible = stamina < MAX_STAMINA
Hiding the bar at full is worth the one line. A HUD element that is permanently at 100% is permanently ignored, and it makes the moment it starts dropping much more legible.
Is this exploitable?
Yes, and it always was. The client owns its own character’s physics — Humanoid.WalkSpeedset on the client replicates because Roblox is designed for that. Someone running a modified client can set it to 200 whether or not you wrote this script. Adding a RemoteEvent so “the server sets the speed” does not change this: the server sets a property the client can immediately overwrite.
So do not build a defence you cannot hold. What you can do, if speed is worth something in your game, is validate the outcome on the server — positions per second between samples, checkpoint times, the actual thing being cheated for — and act on a sustained pattern rather than a single frame. For an obby, a hangout, a showcase, or anything without a competitive reward, this is not worth writing.
One last thing: you cannot automatically test this
Worth knowing before you rely on any tool that says it verified a sprint for you. A Roblox Studio plugin is not permitted to synthesise keyboard input — VirtualInputManagerthrows for anything that is not Roblox’s own code. So a sprint that only misbehaves when you actually hold Shift passes every automated check that can exist, including ours.
Hold the key yourself. That is the test.