This page is for Go developers who need recurring work: poll an API every few seconds, flush metrics every minute, or run maintenance on a schedule. The standard library time package covers fixed periods with time.Sleep and time.NewTicker; stopping cleanly usually means context or a done channel. When the requirement is calendar-based (“every weekday at 09:00”), use a cron library rather than stacking tickers. For ticker semantics and drift in depth, see Golang ticker.
Tested on: Go 1.22, 64-bit Linux.
Quick answer: run a function on a fixed interval
Use time.NewTicker(interval) when you want the same function to run again after each period (every second, every minute, and so on). Receive ticks on ticker.C, run your job, and call ticker.Stop() when finished. For a simple pause between iterations of the same goroutine, time.Sleep is enough. For “every day at 9:00” wall-clock rules, use github.com/robfig/cron/v3 instead of a ticker.
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
// for { select { case <-ticker.C: doWork(); case <-ctx.Done(): return } }Choose the right interval pattern
| Need | Prefer |
|---|---|
| Same delay after each iteration finishes | time.Sleep in the loop body |
| Fixed period between tick starts (may skip if slow) | time.NewTicker |
One shot after a delay, or timeout in select |
time.After or time.NewTimer |
| Background loop with shutdown | Ticker or Sleep + context.Context |
| “At 09:00 daily”, cron strings, weekdays | robfig/cron/v3 |
Short rule: ticker for steady intervals, sleep for simple spacing, timer/After for one-off delays, cron for clock-time rules.
Run every X seconds or every minute with time.NewTicker
time.NewTicker(d) sends the current time on ticker.C after each period d (d must be positive or NewTicker panics). The runtime may drop ticks if the receiver blocks longer than d—that is different from “sleep after work completes.”
Common periods:
| Requirement | d |
|---|---|
| Every second | 1 * time.Second |
| Every 5 seconds | 5 * time.Second |
| Every minute | 1 * time.Minute |
| Every 15 minutes | 15 * time.Minute |
| Every hour | 1 * time.Hour |
Example: print a heartbeat every 2 seconds for about 10 seconds, then stop.
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
stop := time.After(11 * time.Second)
for {
select {
case t := <-ticker.C:
fmt.Println("tick at", t.Format(time.RFC3339))
case <-stop:
fmt.Println("stopping")
return
}
}
}You should see roughly five tick lines about two seconds apart, then stopping.
time.Sleep vs time.Ticker vs time.After
| API | Role |
|---|---|
time.Sleep(d) |
Blocks this goroutine for d |
time.After(d) |
<-chan time.Time that fires once after d (good inside select) |
time.NewTimer(d) |
One-shot timer you can Stop or Reset |
time.NewTicker(d) |
Repeats every d on ticker.C |
time.Sleep(1 * time.Second) matches “pause one second” between steps. time.NewTicker(10 * time.Second) matches “wake every ten seconds” even if your handler sometimes runs quickly. time.After is a common way to cap total runtime or combine with a ticker in one select.
Avoid time.Tick(d) in long-lived programs: it builds a ticker you never stop, which leaks the ticker’s goroutine (time.Tick documentation). Prefer NewTicker plus Stop.
Run repeated work in a goroutine
Run the blocking loop in a background goroutine so main (or your server) can do other work. Use one goroutine consuming ticker.C, not a new goroutine per tick, unless overlapping runs are intentional.
package main
import (
"context"
"fmt"
"time"
)
func runEvery(ctx context.Context, d time.Duration, job func()) {
t := time.NewTicker(d)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
job()
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
go runEvery(ctx, 2*time.Second, func() {
fmt.Println("job", time.Now().Format("15:04:05"))
})
<-ctx.Done()
fmt.Println("done")
}You should see several job lines every two seconds until the context times out, then done. For production services, prefer signal.NotifyContext or a parent context from HTTP shutdown. See context in Go.
Stop interval work cleanly
- Call
ticker.Stop()when you no longer need ticks; it frees the ticker and stops future sends. Stopdoes not closeticker.C; your loop should also return onctx.Done()or adonechannel so you do not block forever after stopping.- If another goroutine must wait for the worker to exit, use
sync.WaitGroupor adonechannel closed after the loop returns.
The first example in this page uses time.After to leave the loop; the context example uses ctx.Done().
Avoid overlapping task runs
If one run can last longer than the ticker period, the next tick may arrive while work is still running.
| Strategy | When to use |
|---|---|
| Wait (serial) | Run the next tick only after the previous job() returns—simplest, may backlog ticks |
| Skip | If job is still running, ignore the tick (track a sync.Mutex or atomic flag) |
| Timeout | Cancel job with a derived context if it exceeds d |
| Overlap | Launch a new goroutine per tick only when re-entrancy is safe (often not for I/O to the same resource) |
Do not spawn unbounded goroutines from ticker.C unless you mean to overlap work and can bound concurrency.
Schedule tasks at specific times (robfig/cron/v3)
A ticker fires every d from when you start it, not “every day at 09:00.” For wall-clock schedules, cron expressions, or presets like @hourly, use github.com/robfig/cron/v3.
go get github.com/robfig/cron/v3The example below is marked {run=false} because it pulls in a third-party module and is not meant for the in-browser Run control.
package main
import (
"fmt"
"time"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
_, _ = c.AddFunc("@every 1m", func() {
fmt.Println("cron tick", time.Now().Format(time.RFC3339))
})
c.Start()
time.Sleep(3 * time.Second)
ctx := c.Stop()
<-ctx.Done()
}Run locally in a module after go get; you should see at least one cron tick line in a few seconds. Increase Sleep if you want to observe multiple minute-aligned runs.
Common mistakes
Using only time.Sleep when you need fast cancellation
Sleep does not listen to context.Done(). Combine Sleep with select and ctx.Done(), or use a ticker plus context.
Calling time.Tick in a long-lived loop
time.Tick leaks if the ticker is never stopped. Use NewTicker and Stop.
Creating NewTicker inside a tight loop without Stop
Each iteration leaks a ticker goroutine. Create one ticker outside the loop.
Forgetting ticker.Stop()
Stops future ticks and releases resources; pair with exiting the loop.
Using a ticker for exact calendar times
Use cron for “every Monday 10:00” style requirements.
Overlapping dangerous work
Email sends, config reloads, and single-file writes should usually not overlap blindly—serialize or skip.
Go interval task cheat sheet
| Goal | Approach |
|---|---|
| Every second | time.NewTicker(1 * time.Second) |
| Every X seconds | time.NewTicker(x * time.Second) |
| Every minute | time.NewTicker(1 * time.Minute) |
| Pause 1 second | time.Sleep(1 * time.Second) |
One delay in select |
case <-time.After(d): |
| Stop loop | context cancel, ticker.Stop, return |
| Background worker | goroutine + ticker + select |
| Daily / cron | robfig/cron/v3 |
| No overlap | one worker or skip-if-busy |
Which pattern should you use?
| You said… | Start with |
|---|---|
| “Every 30 seconds forever” | NewTicker(30 * time.Second) + select |
| “Sleep between retries” | Sleep in loop + backoff if needed |
| “Stop when server shuts down” | context + Stop |
| “9 AM weekdays” | cron library |
| “Work sometimes takes 2 minutes, period is 1 minute” | skip, queue, or extend period—do not stack goroutines blindly |
Summary
Repeated tasks in Go are usually time.NewTicker for steady intervals, time.Sleep when you only need a gap between iterations, and time.After / time.NewTimer for one-shot delays or timeouts inside select. Run tickers in a single goroutine, stop them with ticker.Stop, and combine with context for clean shutdown. When work can exceed the period, decide explicitly whether to wait, skip, or allow overlap. For calendar-based schedules, use robfig/cron/v3 rather than stretching tickers. Prefer NewTicker over time.Tick so resources are released.

