---
title: "cron против time.Ticker и NewTimer"
description: "time ticker go против cron: тикер для равных интервалов, календарный планировщик для расписаний, таймер для одноразовых отсчётов — критерии выбора."
course: "Фоновые задачи в Go: планировщик на robfig/cron"
url: "https://golangmaster.ru/courses/cron/lessons/24-ticker-vs-cron/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# cron против time.Ticker и NewTimer

*Курс «Фоновые задачи в Go: планировщик на robfig/cron», урок 24 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/cron/lessons/24-ticker-vs-cron/*

Выбор между time ticker go и cron в Go-сервисе путают реже, чем стоило бы: тикер и таймер есть в стандартной библиотеке без зависимостей, поэтому в маленьком проекте им отдают всё, а потом задачу «отчёт в девять утра» привязывают к моменту запуска процесса. Обратная крайность — cron для heartbeat раз в секунду, где строка выражения ничего не добавляет, а планировщик тратит память впустую. Инструменты решают разные задачи: тикер отмеряет равные интервалы, таймер — один отсчёт, cron привязывает работу к календарю с часовыми поясами и человеческим конфигом. Разберём контракты всех трёх, арифметику дрейфа и перекрытий и критерии, по которым выбирается механизм для конкретной задачи.

Сценарий для старта: в сервисе отчётов задача «раз в сутки» была сделана на time.NewTicker с интервалом в двадцать четыре часа. Год всё работало, пока команду не перевели на новый деплой: контейнер стал перезапускаться днём после выката релиза, и отчёт сдвинулся с шести утра на четырнадцать часов дня — ровно на момент старта процесса. Второй инцидент в соседней команде: накопитель метрик на тикере с секундным интервалом завис при медленной базе, а тики продолжали сыпаться — никто не заметил, что половина из них теряется мимо занятого потребителя. Оба инцидента — не ошибка кода, а подмена инструмента: в первом случае нужен был календарный механизм, во втором — контроль перекрытий. Дальше — контракты, которые отличают эти инструменты.

### time.Ticker: тикер как планировщик

time.Ticker в Go — это канал, в который runtime отправляет значение каждые T с момента создания: равные интервалы от старта, без привязки к часам суток. Канал тикера буферизован на один элемент — второй тик, пришедший к непрочитанному первому, отбрасывается. Это осознанное решение: тикер — механизм уведомлений «пора смотреть», а не очередь заданий. Пара правил сопровождает каждый тикер: останавливать через Stop, когда он больше не нужен, — из [урока про утечки горутин](/courses/cron/lessons/20-utechki-gorutin/) видно, что случается с забытыми тикерами, — и читать канал в одном месте, потому что два читателя делят тики между собой. Устройство каналов и select разбирает [урок про каналы](/courses/go-advanced/lessons/05-kanaly/); здесь важно, что цикл с select по тикеру и отмене контекста — стандартный каркас воркера на равных интервалах.

### Воркер на тикере с отменой

```go
func sweeper(ctx context.Context, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            sweep() // чистка устаревших сессий
        case <-ctx.Done():
            return
        }
    }
}
```

Каркас тикерного воркера: создание, отложенный Stop, цикл select из двух веток. Ветка тика выполняет работу, ветка отмены закрывает воркер вместе с сервисом — контракт из [урока про контекст](/courses/cron/lessons/10-kontekst-zadachi/). Интервал здесь — величина из кода: чтобы изменить частоту, нужен релиз, и это первый пункт в сравнении с cron.

### Проверка: тики при медленном потребителе

**Вопрос:** Потребитель обрабатывает каждый тик пять секунд, интервал тикера — одна секунда. Что происходит с тиками между завершениями обработки?

- Копятся в буфере канала и обрабатываются потом по очереди
- ✅ Буфер канала держит один тик, остальные отбрасываются: потребитель видит часть тиков
- Тикер приостанавливается, пока потребитель занят
- Паника при переполнении канала

Канал тикера буферизован на один элемент. Пока потребитель занят, тики прибывают и отбрасываются, а не выстраиваются в очередь: когда обработка завершится, потребитель прочтёт самый свежий из накопившихся. Тикер — уведомление «пора работать», а не очередь задач; если пропуск тиков недопустим, механизм выбран неверно.

### Дрейф и перекрытие: арифметика тикера

Тикер отмеряет интервалы от своего старта, и этот факт порождает два следствия. Первое — дрейф: рестарт процесса сдвигает все последующие тики на момент рестарта, и «раз в сутки» оказывается «раз в сутки от запуска». Второе — перекрытие: если работа длится дольше интервала, потребитель не успевает, тики теряются, и фактическая частота падает до скорости обработки. Лечится пара по-разному: перекрытие — флагом занятости или обёрткой с пропуском, а желание «не терять тики» — шаблоном с NewTimer и Reset, где интервал отмеряется от конца работы. Этот шаблон называют равными интервалами с задержкой: работа занимает сколько-то секунд, и следующий отсчёт стартует после неё. Цена та же, что и у дрейфа, — привязка к моменту старта: ни тикер, ни таймер не знают про шесть утра.

### Равные интервалы без потери тиков

```go
func poller(ctx context.Context, interval time.Duration) {
    timer := time.NewTimer(interval)
    defer timer.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-timer.C:
            poll() // работа любой длительности
            timer.Reset(interval) // отсчёт от конца работы
        }
    }
}
```

Timer срабатывает один раз, после работы перезапускается через Reset: тики не теряются, потому что следующий отсчёт начинается после завершения обработки. Интервал между запусками работы равен сумме времени работы и паузы, поэтому частота дрейфует вместе с длительностью работы. Выбор между тикером и этим шаблоном — выбор между «фиксированные метки, часть тиков можно потерять» и «без потерь, но с дрейфом».

### Сколько тиков ждёт в канале?

```go
ticker := time.NewTicker(time.Second)
defer ticker.Stop()

time.Sleep(3500 * time.Millisecond)

fmt.Println(len(ticker.C))
```

**Что выведет программа?**

- ✅ 1
- 3
- 4
- 0

За три с половиной секунды тикер отправил в канал три тика — на первой, второй и третьей секунде. Читателя нет, а буфер канала один, поэтому каждый следующий тик вытеснял предыдущий. К моменту печати в канале лежит ровно один элемент — самый свежий. Этой строчкой проверяется и поведение буфера, и то, что канал не растёт без предела.

### cron против тикера: разница в контракте

cron vs ticker — разница не в удобстве, а в контракте. Планировщик привязывает запуск к календарю: минута, час, день недели, пояс — и переживает рестарты, потому что строка расписания хранится в конфиге, а не в памяти процесса. Тикер привязывает запуск к моменту собственного создания и вместе с ним умирает. Смена расписания без релиза доступна только cron: перезагрузка конфига из [урока про изменение расписания](/courses/cron/lessons/15-izmenenie-raspisaniya/) меняет частоту и время задач, тогда как интервал тикера зашит в бинарник. Взамен тикер даёт простоту и микросекундные накладные расходы: heartbeat, сбор метрик, батч-выгрузки каждые сто миллисекунд — задачи, где календарная привязка бессмысленна, а строка выражения только мешала бы. Часовые пояса и переводы часов — территория планировщика: механика из [урока про пояса](/courses/cron/lessons/07-chasovye-poyasa/) работает на строках расписания, тикеру про зоны нечего сказать.

### NewTimer: одноразовые отсчёты

time.NewTimer — третий инструмент: один отсчёт до истечения длительности, значение в канал, конец. Родственная time.AfterFunc вызывает функцию по истечении в отдельной горутине. Типичные задачи таймера — «через N после события»: напоминание о броне за час до начала, повторный опрос через пять минут, отсроченная отправка. Это противоположность планировщика: cron отвечает на вопрос «когда по календарю», таймер — «через сколько от события», и событие чаще всего случается вне расписания. Для накопления напоминаний многих пользователей строится карта таймеров или мини-очередь по срокам — механика разобрана в [уроке про напоминания бота](/courses/telegram-bot/lessons/19-napominaniya/). Правила гигиены те же: Stop для отменённого таймера, Reset для переиспользования, осознанность с time.After в горячих циклах — из [урока про утечки](/courses/cron/lessons/20-utechki-gorutin/).

### Проверка: напоминание через два часа

**Вопрос:** Пользователь создал событие, напоминание должно прийти через два часа после него. Какой механизм подходит?

- Тикер с интервалом два часа на каждый чек
- Выражение cron 0 */2 * * * и список ожидания
- ✅ time.NewTimer или time.AfterFunc на два часа от события
- Горутина со Sleep на два часа

Отсчёт привязан к моменту события, который случается произвольно, — это контракт таймера. Тикер и cron не знают про событие и дают лишь равные или календарные метки, к которым пришлось бы докручивать список ожидания. Голая горутина со Sleep немасштабируема и не отменяется: таймер даёт и Stop, и Reset.

### Тикер против cron против таймера

| Свойство | time.Ticker | cron | time.Timer |
| --- | --- | --- | --- |
| момент запуска | равные интервалы от старта | календарное расписание | через N от события |
| привязка к часам суток | нет | есть: час, день, пояс | нет |
| конфиг для человека | число в коде | строка crontab в конфиге | длительность |
| перекрытие запусков | тики теряются, разруливать самому | обёртки SkipIfStillRunning | не бывает |
| смена без релиза | нет | конфиг и перезагрузка | нет |
| типичные задачи | heartbeat, метрики, батчи | отчёты, бэкапы, рассылки | напоминания, отложенные повторы |

### Спорное: тикер вместо cron в маленьком сервисе

Спорная привычка, которую стоит оспорить: в маленьком сервисе тикер проще, а cron — лишняя зависимость. Аргумент честный: стандартная библиотека не требует go get, а строка расписания — мини-язык, который приходится объяснять коллегам. Но цена тикера в календарных задачах выше, чем выглядит: привязка к моменту старта, потеря расписания при рестарте, отсутствие человеческого формата в конфиге и ручное разруливание перекрытий — это строки кода, которые пишутся поверх тикера и повторяют cron хуже. Верное чтение границы: тикер — выбор по природе задачи, а не по размеру проекта. Равные интервалы без календарной привязки, высокая частота, работа «сразу после старта и с той же частотой» — территория тикера. Привязка к часу, дню недели или поясу, изменение частоты без релиза, задачи, о которых говорит конфиг, — территория cron, и размер сервиса на выбор не влияет.

> **Тикер на сутки привязывает работу к деплою.** Кейс из первого абзаца, разобранный до конца: тикер с интервалом 24h стартовал вместе с процессом, поэтому отчёт выходил в момент запуска сервиса. Месяцами совпадение было удачным, затем деплой переехал на дневное окно — отчёты ушли на середину дня, а NTP-коррекция часов на хосте добавила редкие сдвиги на минуты. Симптом-подсказка: задача «сдвигается вместе с рестартами» — верный признак тикера там, где нужен календарь. Лечение: строка выражения в конфиге и планировщик из [урока про знакомство](/courses/cron/lessons/01-znakomstvo-cron-go/); расписание становится видимым, редактируемым и переживает рестарты без сдвига.

### Чеклист выбора механизма

- Равные интервалы с высокой частотой и без календарной привязки — time.Ticker.
- Календарное расписание с привязкой к часу, дню и поясу — cron со строкой в конфиге.
- Одноразовый отсчёт от события — time.NewTimer или time.AfterFunc.
- Тикер и таймер создаются с defer Stop на соседней строке.
- Перекрытия тикера разруливаются явно: флаг занятости, обёртка или шаблон с Reset.
- Изменение расписания без релиза возможно только через конфиг планировщика.

### Соберите цикл без потери тиков

Впишите остановку таймера, чтение его канала и перезапуск после работы.

```go
timer := time.NewTimer(interval)
defer timer.___()
for {
    select {
    case <-timer.___:
        work()
        timer.___(interval)
    case <-ctx.Done():
        return
    }
}
```

Подсказка: Освобождение — timer.Stop; канал таймера — timer.C; новый отсчёт — timer.Reset.

Правильные ответы в пропусках: `Stop`, `C`, `Reset`

### Расставьте каркас тикерного воркера

Расставьте строки в порядке файла: создание тикера, освобождение, цикл, выбор, чтение тика и работа.

Правильный порядок строк:

1. `ticker := time.NewTicker(interval)`
2. `defer ticker.Stop()`
3. `for {`
4. `    select {`
5. `    case <-ticker.C:`
6. `        worker()`

### Карточки: тикер, таймер, cron

- **time.Ticker** — Канал с тиком каждые T от момента создания. Равные интервалы без привязки к календарю; Stop обязателен, канал буферизован на один тик.
- **Потерянные тики** — Канал тикера держит один элемент: занятый потребитель теряет часть тиков, а не копит их. Тикер — уведомление, а не очередь заданий.
- **Шаблон с Reset** — NewTimer плюс Reset после работы: тики не теряются, но интервал отмеряется от конца работы, и частота дрейфует вместе с ней.
- **time.Timer** — Одноразовый отсчёт: значение в канал или вызов через AfterFunc по истечении. Отсчёт от события, а не от календаря; Stop отменяет, Reset перезапускает.
- **cron против тикера** — Календарная привязка, пояс, человеческий конфиг и смена без релиза — у cron. Равные интервалы, высокая частота и нулевые зависимости — у тикера.
- **Признак неверного инструмента** — Задача сдвигается вместе с рестартами процесса — это тикер там, где нужен календарь. Секундные метки в строке cron — планировщик там, где хватит тикера.

> **Где запускать примеры.** Это самый дружелюбный песочнице урок: оба предикта исполняются там как есть, потому что пакет time доступен целиком — тикеры, таймеры, len канала и Sleep с миллисекундными интервалами. Уменьшите интервалы до сотен миллисекунд и воспроизведите оба инцидента: потерю тиков занятым потребителем и перезапуск по Reset. Воркеры с контекстом и каналами тоже проверяются на месте — select и close работают честно. Живой cron-планировщик и перезагрузка конфига исполняются локально из модуля курса.

Выбор механизма фоновой работы — чтение контракта, а не привычка: тикер отмеряет равные интервалы от старта и теряет тики занятого потребителя, таймер отсчитывает от события, cron привязывает работу к календарю с поясами и живёт в конфиге. Признаки перепутанных ролей легко ловятся: расписание, сдвигающееся с деплоем, и секундные метки в строках выражений. Дальше по маршруту: [итоговый проект](/courses/cron/lessons/25-itogovyj-proekt/) — сборка сервиса задач, где оба мира встречаются: календарные отчёты и минутные напоминания в одном планировщике. Если возвращались с пропуска — [пакет time](/courses/go-advanced/lessons/23-time/) даёт полную арифметику таймеров, [синтаксис выражений](/courses/cron/lessons/03-sintaksis-cron/) — словарь расписаний, а [секунды и NewWithSeconds](/courses/cron/lessons/08-sekundy-raspisaniya/) показывают, когда cron спускается до секундных интервалов. Полный маршрут — [на странице курса](/courses/cron/). В домашнем задании — ревизия своих таймеров по чеклисту.

## Коротко: что запомнить

- Тикер отмеряет равные интервалы от момента создания: рестарт сдвигает метки, календаря у него нет.
- Канал тикера буферизован на один тик: занятый потребитель теряет тики, а не копит их.
- Шаблон с NewTimer и Reset убирает потери, но интервал дрейфует вместе с длительностью работы.
- Таймер — один отсчёт от события: напоминания, отложенные повторы, Stop и Reset как часть контракта.
- cron даёт календарную привязку, пояса, человеческий конфиг и смену расписания без релиза.
- Выбор по природе задачи: интервалы — тикер, календарь — cron, отсчёт от события — таймер.

## Домашнее задание

Проведите ревизию фоновых механизмов своего проекта: выпишите каждый тикер, таймер и задачу по расписанию и классифицируйте по таблице урока — интервал, календарь, отсчёт от события. Найдите календарные задачи, сделанные на тикере, и переведите их на cron со строкой в конфиге; для проверки сдвига отметьте момент старта процесса и сравните с моментом задачи. Для одного тикера постройте шаблон с Reset и измерьте фактическую частоту под нагрузкой, где работа длиннее интервала: убедитесь в потере тиков у тикера и их сохранении у шаблона. Финал — добавьте к тикерному воркеру ветку отмены по контексту и убедитесь, что Stop вызывается на выходе.

## Вопросы и ответы по уроку

### Почему тики тикера теряются, а не ждут в очереди?

Канал тикера буферизован ровно на один элемент: новый тик при занятом канале отбрасывается. Это решение стандартной библиотеки под контракт уведомлений: потребитель хочет знать, что пора работать, а не получить очередь из ста пропущенных меток, которые устарели ещё при прибытии. Нужна очередь — она строится явно, отдельной структурой.

### Можно ли тикером делать задачу «раз в сутки в шесть утра»?

Можно, но контракт будет неверным: интервал в двадцать четыре часа привязан к моменту создания тикера, и при каждом рестарте задачи метка сдвигается. Часовой пояс и перевод часов тоже остаются вне контракта. Для календарной привязки нужен планировщик со строкой выражения — тогда шесть утра остаётся шестью утра независимо от деплоев.

### Когда тикер действительно лучше планировщика?

Когда задача по природе равных интервалов: heartbeat и здоровье, сбор метрик, батч-выгрузки, опрос по частоте в десятки герц. Календарная привязка там бессмысленна, человеческий конфиг не нужен, а накладные расходы планировщика на частых тиках были бы заметны. Тикер — стандартная библиотека без зависимостей, и в этих сценариях это весомый аргумент.
