What "Loop time of 0.02s overrun" and watchdog-not-fed warnings actually mean in FRC, how to read WPILib's epoch dump, and the usual causes.
to read
words
sections
"Loop time of 0.02s overrun" means your robot program's main loop took longer than 20 milliseconds, the default period WPILib's TimedRobot gives itself to run through the current mode's periodic method plus some housekeeping. It's a warning, not a crash. Your code kept running, it just missed its deadline. One overrun right when you deploy or first enable is almost always harmless: Java's JIT compiler and class loading eat real time on the very first pass through your code, and teams report that exact one-time blip constantly on Chief Delphi without it causing any actual problem. If it keeps happening, especially every loop or in bursts while you're driving, something in your code is genuinely too slow, and the good news is WPILib already tells you what, for free, every time it happens.
"Watchdog not fed" and "...Output not updated often enough" are two more messages people lump in with this, and they're not the same thing as the loop overrun or as each other. Knowing which is which saves you from debugging the wrong code path.
TimedRobot builds itself a Watchdog object using your loop's period (0.02 seconds by default) and a callback. In the actual WPILib source, that callback is one line:
private void printLoopOverrunMessage() {
DriverStation.reportWarning("Loop time of " + m_period + "s overrun\n", false);
}
That's why the number in the message always matches your configured period, not how late you actually were. The same Watchdog object can also print a second, more generic message, "Watchdog not fed within 0.020000s", when it isn't reset in time. Both come from the same timeout, but the generic one is heavily rate-limited in the WPILib source, so in practice you'll see "Loop time of 0.02s overrun" print every single time the loop misses, while the "not fed" version shows up rarely, if ever.
"...Output not updated often enough" is a different mechanism entirely: MotorSafety. DifferentialDrive, MecanumDrive, and the other WPILib drive helpers all extend it, and per WPILib's own docs, "MotorSafety is enabled by default. The tankDrive, arcadeDrive, or curvatureDrive methods should be called periodically to avoid Motor Safety timeouts." Its default expiration is 100 milliseconds (kDefaultSafetyExpiration = 0.1 in the source), five times longer than the loop watchdog's 20ms, and it's tracked per actuator instead of for the whole loop. If your code stalls long enough that a drivetrain object stops getting .arcadeDrive() or .set() calls, that object shuts itself off and prints this message naming itself, which is actually a useful diagnostic on its own: it tells you which subsystem stopped updating without needing an epoch dump at all.
You don't need to comment out half your robot code to bisect the problem. WPILib already breaks the loop into pieces and times each one, every time it overruns, automatically. Right after a "Loop time" warning, look further down the Driver Station Console Viewer or RioLog for a block like this:
Loop time of 0.02s overrun
teleopPeriodic(): 0.048123s
robotPeriodic(): 0.001204s
SmartDashboard.updateValues(): 0.021880s
LiveWindow.updateValues(): 0.000031s
Shuffleboard.update(): 0.000019s
That table comes straight from TimedRobot's own watchdog, which adds a named epoch after every stage of the loop: whichever mode-specific periodic function ran, then robotPeriodic(), then SmartDashboard.updateValues(), LiveWindow.updateValues(), and Shuffleboard.update(), in that order. Each number is the time that single stage took, not a running total, so the outlier line is your answer. In the example above, both teleopPeriodic() and the SmartDashboard call are worth checking.
If teleopPeriodic() is the big number and you're using command-based programming, there's a second, more detailed table one level down. The CommandScheduler keeps its own separate Watchdog, timed to the same period, and if its run() call goes over budget it prints "CommandScheduler loop overrun" straight to the console, followed by its own epoch table:
CommandScheduler loop overrun
DriveSubsystem.periodic(): 0.002001s
IntakeSubsystem.periodic(): 0.031044s
buttons.run(): 0.000412s
DriveCommand.execute(): 0.001998s
Every registered subsystem's periodic() gets its own line, followed by buttons.run() for your button bindings, then each currently scheduled command's execute(). Read it the same way: the outsized number names the method. One catch if you've changed TimedRobot's period away from the 20ms default in its constructor: the scheduler's watchdog doesn't pick that up automatically. You have to call CommandScheduler.getInstance().setPeriod(period) yourself, and WPILib's own docs on that method say directly, "this should be kept in sync with the TimedRobot period."
Once you know which method is slow, it's almost always one of these.
A blocking call inside it. A Thread.sleep(), a while loop spinning on a sensor value, or a CAN call that waits, all stall the entire loop until they return. CTRE's docs on status signals explain that waitForAll() gets its real benefit on a CANivore, where devices synchronize their time bases so signals can be sampled and published together, cutting the latency of a synchronous wait. A plain roboRIO CAN bus doesn't have that synchronization, so a manual wait there is a bigger gamble for your loop time. If you need current data, prefer the signal's automatic background refresh over a manual wait with a nonzero timeout.
Dashboard and console spam. Every SmartDashboard.putNumber() and every System.out.println() costs real time, and it adds up fast inside a subsystem's periodic() if you're pushing several values every single loop. TimedRobot already calls NetworkTableInstance's flushLocal() on its own each cycle, which pushes updates to the local client/server. Its sibling flush() is a different call, it forces an immediate send out over the network, and WPILib's own docs note it's rate-limited to protect the network from flooding. Reach for it only when you specifically need to sync a network update with your own code, not as a routine per-loop call. If you're logging data every loop for later review, prefer DataLogManager, which does its file writes on a separate thread, over printing to the console every cycle.
Vision processing running on the roboRIO itself. If the slow method touches a camera or does any per-frame image processing directly inside robotPeriodic() or a subsystem's periodic(), you're splitting the roboRIO's CPU between vision work and your control loop. WPILib's own vision programming strategy guide says plainly that "having vision code running on the same processor as the robot program can cause performance issues," and recommends a coprocessor instead, since it "can run full speed and not interfere with the robot program." A Limelight or a PhotonVision coprocessor does that work off-board and hands you results over the network, so it never touches your 20ms budget.
Too many CAN reads, or too much CAN traffic overall. Calling a getter on every motor controller and sensor every single loop adds up as your device count grows. REV's docs note that SPARK controllers broadcast several periodic status frames on their own, and recommend turning down the frame rate on signals you don't actually need if you're running a lot of CAN devices. This is a different angle on the same bus covered in our CAN bus guide, which walks through wiring, termination, and ID conflicts. The issue here isn't wiring, it's how much CAN traffic your loop has to wait on every cycle.
For a robot with many subsystems, where no single method is an obvious outlier but the total keeps creeping past 20ms anyway, WPILib has an alternative to TimedRobot called TimesliceRobot. Instead of letting every controller's periodic function compete for the same shared 20ms, it schedules each one into its own dedicated slice of time. It's a more advanced fix, worth reaching for only after you've already gone through the causes above and you're still tight on budget.
MotorSafety, with a 100ms timeout tracked per actuator. The message names the actuator that stalled.This article is AI-assisted: drafted from primary sources, then reviewed and edited by hand. Errors still get through. When one is reported we fix it and write down what changed — publicly, in the corrections log.
Keep going
You came here for one answer. These lessons teach the same subject properly, in the order a team actually learns it. All 51 are free and none of them need an account to read.
Go deeper
LearnFRC has 394 free FRC lessons across every department. Create a free account to save your place, track your progress, and earn a certificate.
Create a free accountKeep reading
Structured lessons and quizzes across every department. Create a free account to save your progress, track your team, and earn a certificate.