Question
In Go, is there a clean way to run a repetitive background task at fixed intervals, similar to Timer.schedule(task, delay, period) in Java?
I know this can be done with a goroutine and time.Sleep(), but I would prefer an approach that is easier to stop cleanly.
For example, I currently have code like this:
func oneWay() {
var f func()
var t *time.Timer
f = func() {
fmt.Println("doing stuff")
t = time.AfterFunc(5*time.Second, f)
}
t = time.AfterFunc(5*time.Second, f)
defer t.Stop()
// Simulate doing other work
time.Sleep(time.Minute)
}
This works, but it feels awkward. Is there a cleaner or more idiomatic way to schedule repeating work in Go and stop it when needed?
Short Answer
By the end of this page, you will understand how to run repeated tasks in Go using time.Ticker, when to use time.Timer instead, and how to stop background work cleanly with channels or cancellation patterns.
Concept
In Go, the usual way to perform work repeatedly at fixed intervals is with time.Ticker.
A Ticker sends the current time on its channel at regular intervals. You can listen for those ticks in a loop and run your task each time one arrives.
time.Timer and time.AfterFunc are better for one-time scheduling. They can be reused in advanced cases, but for simple repeating work they are usually less clear than a ticker.
This matters because background jobs are common in real programs:
- refreshing cache entries
- polling an API
- cleaning expired sessions
- writing periodic metrics
- retrying work on a schedule
In Go, the idiomatic approach is usually:
- run the repeated task inside a goroutine
- trigger it with a
time.Ticker - stop it with
ticker.Stop() - optionally listen for a
donesignal to exit cleanly
This keeps the code readable and makes shutdown behavior explicit.
Mental Model
Think of a Ticker like a metronome.
Every few seconds, it makes a click. Your program waits for each click, then does a piece of work.
A Timer is more like a one-time alarm clock:
- it rings once after a delay
- if you want it again, you must set it again
So if your job is repeat every 5 seconds, a ticker matches the idea directly. If your job is run once after 5 seconds, a timer is the better fit.
Syntax and Examples
The basic syntax for repeated work in Go looks like this:
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
fmt.Println("doing stuff")
}
This loop runs forever, printing every 5 seconds.
A more practical version includes a way to stop:
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan struct{})
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("doing stuff")
case <-done:
fmt.Println("stopping")
return
}
}
}()
time.Sleep(16 * time.Second)
close(done)
time.Sleep(1 * time.Second)
}
What this does
Step by Step Execution
Consider this example:
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
done := make(chan struct{})
go func() {
time.Sleep(7 * time.Second)
close(done)
}()
for {
select {
case t := <-ticker.C:
fmt.Println("tick at", t.Format("15:04:05"))
case <-done:
fmt.Println("finished")
return
}
}
}
Step by step
- A ticker is created with a 2-second interval.
- A
donechannel is created for shutdown. - A goroutine waits 7 seconds, then closes
done. - The main loop starts a
select. - After about 2 seconds,
ticker.Creceives a value.
Real World Use Cases
Repeated interval-based work is common in Go applications.
Polling an external API
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
// fetch latest data
}
Useful for:
- checking payment status
- syncing inventory
- refreshing exchange rates
Cleaning expired data
A web server may remove old sessions every minute.
for range ticker.C {
// delete expired sessions from memory or database
}
Writing metrics or heartbeats
A service may report health every 10 seconds.
- CPU usage
- queue length
- memory stats
- "I am alive" heartbeats
Cache refresh
Applications often refresh cached configuration or feature flags periodically rather than reloading on every request.
Real Codebase Usage
In real projects, developers usually combine interval logic with a few common patterns.
Guard clauses inside the tick handler
case <-ticker.C:
if !isLeader {
continue
}
runCleanup()
This prevents work from running unless certain conditions are true.
Early return on shutdown
case <-done:
return
This is one of the cleanest ways to stop a background goroutine.
Error handling inside periodic jobs
case <-ticker.C:
if err := syncData(); err != nil {
log.Println("sync failed:", err)
}
Background jobs should usually log errors instead of crashing the whole program.
Context-based cancellation
In larger codebases, context.Context is often used instead of a custom done channel.
func worker(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
ticker.Stop()
{
{
<-ticker.C:
doWork()
<-ctx.Done():
}
}
}
Common Mistakes
1. Using time.Sleep() in a manual infinite loop
This works, but is often less flexible to stop cleanly.
for {
time.Sleep(5 * time.Second)
doWork()
}
Why it is a problem:
- shutdown can be delayed until sleep finishes
- cancellation is less explicit
- it is harder to coordinate with other signals
Prefer a ticker with select when you need control.
2. Forgetting to stop the ticker
Broken example:
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
doWork()
}
If the loop exits elsewhere or the goroutine ends unexpectedly, the ticker may keep running longer than needed.
Better:
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
3. Using time.AfterFunc for repeating work when a ticker is clearer
Your original approach reschedules itself:
f = func {
doWork()
t = time.AfterFunc(*time.Second, f)
}
Comparisons
| Tool | Best for | Repeats automatically | Can be stopped | Typical use |
|---|---|---|---|---|
time.Sleep | Simple delays | No | No direct cancellation | Pause current goroutine |
time.Timer | One-time delayed execution | No | Yes | Run once later |
time.AfterFunc | Run a function once after a delay | No | Yes | Fire a callback later |
time.Ticker | Repeated interval-based work | Yes | Yes | Background periodic jobs |
Cheat Sheet
// Repeat every 5 seconds
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doWork()
case <-done:
return
}
}
Quick rules
- Use
time.Tickerfor repeated work. - Use
time.Timerortime.AfterFuncfor one-time delayed work. - Always call
ticker.Stop()when finished. - Use
selectwith adonechannel orctx.Done()for clean shutdown. - Be careful about overlapping work if each tick launches a goroutine.
Common patterns
// One-time delay
<-time.NewTimer(3 * time.Second).C
// Simple repeated loop
for range ticker.C {
doWork()
}
// Stoppable repeated loop
for {
select {
case <-ticker.C:
doWork()
case <-done:
}
}
FAQ
How do I run a function every few seconds in Go?
Use time.NewTicker(interval) and loop over ticker.C, usually inside a goroutine.
What is the difference between time.Timer and time.Ticker in Go?
A timer fires once after a delay. A ticker keeps firing at regular intervals until stopped.
How do I stop a repeating goroutine in Go?
Use a done channel or context.Context, and return from the goroutine when cancellation is signaled.
Is time.Sleep() bad for repeated tasks in Go?
Not always, but it is less convenient for clean shutdown and coordination than time.Ticker.
Does ticker.Stop() close the ticker channel?
No. It stops future ticks, but the channel is not closed.
Can a periodic task overlap with itself?
Yes, if you launch a new goroutine on every tick and the previous run has not finished yet.
Should I use time.AfterFunc for repeated scheduling?
Usually no. It is possible, but time.Ticker is simpler and more idiomatic for fixed intervals.
Mini Project
Description
Build a small background worker in Go that prints a message every 3 seconds and stops cleanly after 10 seconds. This demonstrates the idiomatic pattern for repeated tasks using a ticker and a stop signal.
Goal
Create a stoppable periodic worker using time.NewTicker, a goroutine, and a shutdown channel.
Requirements
- Start a background worker that runs every 3 seconds.
- Print a timestamped message each time the worker runs.
- Stop the worker cleanly after 10 seconds.
- Ensure the ticker is stopped when the worker exits.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.