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
time.NewTicker(5 * time.Second)creates a repeating signal every 5 seconds.- The goroutine waits in a
selectstatement. - When the ticker fires, the task runs.
- When
doneis closed, the goroutine exits. ticker.Stop()prevents the ticker from continuing to send ticks.
If you only need a one-time delay, use a timer instead:
timer := time.NewTimer(5 * time.Second)
<-timer.C
fmt.Println("runs once after 5 seconds")
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. - The program prints the tick time.
- After about 4 seconds, another tick arrives.
- After about 6 seconds, another tick arrives.
- After about 7 seconds, the
donechannel is closed. - The
selectchooses thedonecase. - The program prints
finishedand returns.
Important detail
If your task takes longer than the ticker interval, ticks do not queue up forever in the way many beginners expect. A ticker is a timing signal, not a job scheduler with a backlog. If exact scheduling matters, you may need a different design.
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)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doWork()
case <-ctx.Done():
return
}
}
}
This integrates well with servers, request lifecycles, and graceful shutdown.
Avoid overlapping work
If the task may take longer than the interval, some codebases skip a run if the previous one is still active, or run the task synchronously in the same loop to prevent overlap.
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(5*time.Second, f)
}
This can work, but it is harder to read and reason about than a normal loop with a ticker.
4. Accidentally creating overlapping executions
for range ticker.C {
go doWork()
}
If doWork() takes longer than the interval, multiple copies may run at the same time.
Avoid this unless concurrency is intentional.
5. Assuming ticks are exact forever
Tickers are good for periodic work, but they are not a perfect real-time scheduler. Execution time, CPU load, and blocking operations can affect when work actually runs.
6. Confusing timer and ticker
Timer: one event after a delayTicker: repeated events at an interval
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 |
time.Ticker vs recursive time.AfterFunc
| Approach | Pros | Cons |
|---|---|---|
time.Ticker | Clear, idiomatic, easy to stop | Not ideal for very custom rescheduling |
Recursive time.AfterFunc | Flexible for dynamic next-run timing | Harder to read and manage |
done channel vs context.Context
| Approach | Best for | Notes |
|---|---|---|
done channel | Small programs or focused examples | Simple and explicit |
context.Context | Real applications and libraries | Standard cancellation pattern in Go |
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:
return
}
}
Edge cases
- If work takes longer than the interval, exact timing may drift.
ticker.Stop()stops future ticks, but does not closeticker.C.- If you need immediate first execution, call
doWork()once before starting the loop.
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
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
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.