Golang Run Function at Intervals: Ticker, Sleep, and Cron Examples

Run repeated tasks in Go with time.NewTicker and time.Sleep, stop loops with context, avoid overlapping runs, and use robfig/cron/v3 for wall-clock schedules.

Published

Updated

Read time 6 min read

Reviewed byDeepak Prasad

Golang Run Function at Intervals: Ticker, Sleep, and Cron Examples

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.

go
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.

go
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
		}
	}
}
Output

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.

go
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")
}
Output

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.
  • Stop does not close ticker.C; your loop should also return on ctx.Done() or a done channel so you do not block forever after stopping.
  • If another goroutine must wait for the worker to exit, use sync.WaitGroup or a done channel 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.

text
go get github.com/robfig/cron/v3

The example below is marked {run=false} because it pulls in a third-party module and is not meant for the in-browser Run control.

go
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.


References

Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise …