---
title: "Таймауты и устойчивость"
description: "chromedp таймаут через context.WithTimeout: прогрев первого Run, retry-обёртка с бэкоффом, смерть Chrome и chromedp.Cancel — устойчивый парсер на Go."
course: "Chromedp в Go: автоматизация Chrome без Selenium"
url: "https://golangmaster.ru/courses/chromedp/lessons/15-tajmauty-i-prochnost/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Таймауты и устойчивость

*Курс «Chromedp в Go: автоматизация Chrome без Selenium», урок 15 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/chromedp/lessons/15-tajmauty-i-prochnost/*

chromedp таймаут задаёт не библиотека, а сам язык: контекст с дедлайном из пакета context — и от того, где стоит этот дедлайн, зависит, доедет ли ночное задание до конца или умрёт на первой медленной странице. Разберём перишаговые таймауты, прогрев первого Run, retry-обёртку с бэкоффом, смерть браузера и вежливое закрытие через chromedp.Cancel — фундамент, на котором держится устойчивый парсер.

Обход из двадцати страниц вчерашним вечером сегодня падает на третьей: сеть моргнула, сайт притормозил, Chrome тихо закрылся. Пока программа одноразовая и ручная, это просто перезапуск; но парсер, который работает ночью по расписанию, измеряется не удачными запусками, а процентом заданий, доехавших до конца, и без таймаутов этот процент держится на удаче. Устойчивость в chromedp складывается из трёх вещей, и все три — про контекст. Первая: у каждого действия должен быть потолок времени — дедлайн в контексте, который истечёт сам, без часов и Sleep. Вторая: сбои должны повторяться с растущей паузой, а не валить обход с первой попытки. Третья: смерть браузера — штатная ситуация, из которой программа выходит пересозданием контекста, а не перезапуском всей программы. Всё это уже знакомые механизмы: контексты из первых уроков, ожидания из девятого, аллокаторы из предыдущего. Здесь они собираются в дисциплину: где стоит дедлайн, что ретраить, как заметить мёртвый браузер и как закрывать его по-взрослому.

### chromedp таймаут: дедлайн в контексте

chromedp таймаут получает не отдельной опцией действия, а контекстом: context.WithTimeout(ctx, 30*time.Second) возвращает производный контекст с дедлайном и функцию отмены, и всё, что исполняется с этим контекстом, укладывается в потолок. Механика одна на всю программу: каждое действие chromedp перед командой и во время неё смотрит на контекст, и истёкший дедлайн возвращается из Run ошибкой context.DeadlineExceeded. Проверять её нужно errors.Is, а не сравнением текста: обёртки и ретраи добавляют свои слои, и только errors.Is видит причину сквозь них. Отмена — обязательная пара к дедлайну: defer cancel() освобождает таймер и внутренние структуры, без него контексты копятся до конца программы. И помните про отсчёт: дедлайн стартует от вызова WithTimeout, а не от начала Run — если между созданием контекста и Run прошло двадцать секунд из тридцати, на само действие останется десять. Сообщение у ошибки стабильное — context deadline exceeded, — и вместе с errors.Is оно удобно отделяет дедлайны от прочих сбоев в общем потоке логов. Дедлайны вкладываются друг в друга как матрёшки: общий на обход, перишаговый на страницу, и истечение внешнего погашает внутренние досрочно.

### Перишаговый таймаут в цикле обхода

```go
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()

// прогрев: первый Run заводит браузер — без короткого дедлайна
if err := chromedp.Run(ctx, chromedp.Navigate("about:blank")); err != nil {
        log.Fatal(err)
}

for _, url := range urls {
        stepCtx, stepCancel := context.WithTimeout(ctx, 30*time.Second)
        err := chromedp.Run(stepCtx,
                chromedp.Navigate(url),
                chromedp.WaitVisible(".cards"),
        )
        stepCancel()
        if err != nil {
                log.Printf("%s: %v", url, err)
                continue
        }
        // ... чтение данных страницы
}
```

Дедлайн живёт в stepCtx и действует только на свой шаг: истёк — Run вернёт context.DeadlineExceeded, а браузер продолжит работать под родительским ctx. stepCancel вызывают сразу после Run вручную: таймер освобождается, не дожидаясь конца функции. Ошибка шага пишется в лог вместе с адресом, и цикл идёт дальше — отчёт по сбойным страницам собирается после обхода.

### Прогрев: первый Run без дедлайна

NewContext ничего не запускает: браузер заводится на первом Run — и документация библиотеки прямо предупреждает, что вешать таймаут контекста на первый Run плохая идея: истечение дедлайна остановит весь браузер. Причина в механике: первый Run и есть аллокация — запуск процесса, чтение адреса отладки, рукопожатие по вебсокету; отмена контекста в этот момент гасит всё целиком, и контекст остаётся нерабочим. Отсюда дисциплина прогрева: первый Run исполняет дешёвое действие без дедлайна — Navigate на about:blank достаточно, — и только после него обход идёт перишаговыми контекстами с короткими потолками. Прогрев делается один на контекст: повторять его после каждого шага не нужно, браузер живёт, пока жив родительский контекст, и все следующие Run едут через уже открытое соединение. Холодный старт Chrome на загруженном сервере занимает секунды, поэтому перишаговый дедлайн обязан мерить работу страницы, а не запуск браузера — иначе медленная машина будет ронять обход на первом же адресе, хотя сама страница могла бы открыться мгновенно. И если прогрев упал — Chromium не нашёлся, флаг битый, — ретраить его бессмысленно до исправления окружения: ошибка старта из таблицы ниже придёт снова.

> **Живое место: пять секунд, которые унесли браузер.** Классическая авария: помощник оборачивает весь обход в context.WithTimeout(ctx, 5*time.Second) — «чтобы действия не висели». На быстрой машине всё работает, на холодном сервере первый Run не успевает завести браузер, дедлайн сгорает — и весь браузер умирает вместе с контекстом, потому что первый Run и есть аллокация. Все последующие Run возвращают context canceled, разработчик ищет падение Chrome и утечку памяти, а виноват таймаут не на том контексте. Лечение — прогрев без дедлайна и перишаговые потолки на производных контекстах. Спорное мнение с условием: общий потолок на весь обход ставить можно и нужно — но поверх прогретого браузера и с запасом на аллокацию.

### Retry-обёртка с бэкоффом

```go
func withRetry(parent context.Context, attempts int,
        act func(context.Context) error) error {
        delay := time.Second
        var err error
        for i := 0; i < attempts; i++ {
                tctx, cancel := context.WithTimeout(parent, 20*time.Second)
                err = act(tctx)
                cancel()
                if err == nil {
                        return nil
                }
                if errors.Is(err, context.Canceled) {
                        return err // браузер умер или обход отменили — ретрай не спасёт
                }
                select {
                case <-parent.Done():
                        return parent.Err()
                case <-time.After(delay):
                }
                delay *= 2
        }
        return fmt.Errorf("после %d попыток: %w", attempts, err)
}
```

Каждая попытка получает собственный дедлайн и свежий производный контекст: таймаут прошлой попытки не портит следующую. context.Canceled выводится из ретрая отдельно — это сигнал внешней отмены или смерти браузера, повторять бессмысленно. Пауза растёт вдвое после каждой неудачи и уважает отмену родителя через select. Ошибка последней попытки оборачивается через %w, поэтому errors.Is на стороне вызывающего видит первопричину.

### Ретраи: что повторять, а что чинить

Устойчивый парсер отличает повторяемую ошибку от неповторяемой, и классификация здесь короткая. Таймаут шага или моргание сети — повторять: у страницы может не быть удачи в эту секунду, и свежая попытка с новым дедлайном часто проходит. ErrPollingTimeout из девятого урока — не ретраить вслепую: предикат не стал истиной за свои тридцать секунд, значит селектор указывает мимо или сайт отдаёт не то, что вы ждёте, и повторение вернёт ту же ошибку на те же деньги. context.Canceled — красный флаг: контекст отменили до дедлайна — либо вы сами, либо chromedp при потере браузера, об этом ниже. Ошибка старта процесса — территория конфигурации: ретраем её не лечат. У библиотеки тот же приём стоит внутри: Query-механика крутит внутренний хелпер retryWithSleep с интервалом в несколько миллисекунд, но публичного API ретраев в chromedp нет — обёртка пишется на Go руками, и она у вас уже есть из примера выше. Потолок попыток — обязательный атрибут обёртки: без него одна навечно сломанная страница превратит обход в бесконечный цикл, который не спасает даже бэкофф.

### Классификация ошибок устойчивого обхода

| Ошибка | Откуда | Что делать |
| --- | --- | --- |
| context.DeadlineExceeded | истёк дедлайн шага | ретрай с бэкоффом и свежим дедлайном |
| context.Canceled | отмена снаружи или смерть браузера | не ретраить: восстанавливать контекст |
| waiting for function failed: timeout | ErrPollingTimeout: предикат не стал истиной | селектор и сайт, не сеть: слепой ретрай бесполезен |
| websocket url timeout reached | Chrome не отдал адрес отладки за 20 секунд | путь Chrome и окружение запуска |
| chrome failed to start: ... | процесс не стартовал | флаги, память, права — конфигурация, не сеть |

### Смерть браузера: заметить и восстановиться

Chrome — внешний процесс, и он умирает: OOM-killer на сервере, крэш вкладки, обрыв соединения. У chromedp на этот случай правило из исходников: как только соединение с браузером потеряно, библиотека сама отменяет контекст — процесс связан с контекстом, и отмена гасит его. Для парсера это выглядит узнаваемо: действие, которое ещё вчера работало, возвращает context canceled, а вы cancel не вызывали. Восстановление — пересоздание: старый контекст мёртв навсегда, но аллокатор жив, и новый NewContext от того же аллокатора заводит свежий браузер с теми же флагами — та же механика, что в ротации личностей из четырнадцатого урока про [прокси и User-Agent](/courses/chromedp/lessons/14-proksi-ua/) и в пуле из девятнадцатого. Различать «нашу» отмену и смерть браузера проще всего по дизайну: если программа сама не вызывала cancel, а context canceled пришёл — браузер умер. Это сигнал к восстановлению, а не к ретраю: попытки уезжать действиями в мёртвый контекст вернут ту же ошибку. Для мониторинга заведите счётчик пересозданий контекста за задание: единичные смерти — норма внешнего мира, а рост от задания к заданию — проблема окружения, память, версия Chrome или лимиты.

### chromedp.Cancel: закрыть браузер и увидеть ошибку

defer cancel() закрывает контекст, но молча: ошибки, случившиеся при уборке, теряются. chromedp.Cancel — взрослая версия той же отмены: она закрывает браузер штатно, дожидается, пока уберутся ресурсы — включая временный каталог профиля, — и возвращает ошибку, если при закрытии что-то пошло не так. Закрытие само можно ограничить дедлайном: документация показывает шаблон с WithTimeout на десять секунд вокруг Cancel — если браузер не закрылся за это время, вы узнаете об этом по своему таймеру. Отдельный случай — Cancel на контексте аллокатора: там нет chromedp-контекста, и Cancel вернёт ErrInvalidContext; аллокатор закрывается собственной функцией отмены. Где это нужно: при перезапуске браузера посреди обхода — убить подозрительного штатно и завести нового от аллокатора; при завершении задания — получить ошибку уборки на руки и записать в лог, а не потерять при выходе из программы.

### Штатное закрытие с дедлайном

```go
tctx, tcancel := context.WithTimeout(ctx, 10*time.Second)
defer tcancel()
if err := chromedp.Cancel(tctx); err != nil {
        log.Printf("закрытие браузера: %v", err)
}
// браузер закрыт штатно, ресурсы убраны — можно заводить нового
newCtx, newCancel := chromedp.NewContext(allocCtx)
defer newCancel()
```

Cancel — вариант defer cancel() с отчётом: закрывает браузер штатно, дожидается уборки ресурсов и возвращает ошибку, если что-то пошло не так при закрытии. Дедлайн на tctx ограничивает ожидание десятью секундами — это шаблон из документации библиотеки. Новый контекст заводится от аллокатора: это свежий чистый браузер с тем же набором флагов личности.

> **Два измерения ожидания.** Таймауты живут в двух измерениях, и путать их дорого: перишаговый дедлайн отвечает за одну страницу, общий — за весь обход. Вложите один в другой: дедлайн шага меньше общего, тогда истечение шага не убивает задание целиком. Рядом живут и [Poll-потолки](/courses/chromedp/lessons/09-ozhidaniya-elementov/) из девятого урока — WithPollingTimeout и контекстный дедлайн конкурируют, сработает тот, что раньше; выбирайте под смысл: предикат ждёт факт, дедлайн страхует всё остальное. Потолок для шага дешевле вывести из лога, чем угадать: пишите затраченное время по каждому шагу — распределение подскажет границу точнее любого совета.

### Чек-лист устойчивого обхода

- прогрев: первый Run — без дедлайна, он заводит браузер
- дедлайны шагов — на производных контекстах: истёк шаг, браузер жив
- ошибки шагов — в лог с адресом; обход не падает, отчёт в конце
- ретрай — для таймаутов и сетевых морганий; context.Canceled — не ретрай
- пауза ретрая растёт вдвое и уважает отмену родителя
- context canceled без вашей вины — браузер умер: новый NewContext от аллокатора
- закрытие по-взрослому — chromedp.Cancel с дедлайном: штатно и с ошибкой на руках

### Проверка: первый Run под таймаутом

**Вопрос:** Контекст обёрнут в context.WithTimeout на 2 секунды, и это первый Run: холодный старт Chrome занимает дольше. Что произойдёт?

- Браузер заведётся, а Run вернёт ошибку таймаута
- ✅ Дедлайн отменит контекст — браузер остановится целиком
- Таймаут игнорируется на первом Run
- chromedp автоматически продлит таймаут на аллокацию

Первый Run аллоцирует браузер, и документация прямо предупреждает: таймаут контекста на первом Run остановит весь браузер. Поэтому обход начинают прогревом — Run без дедлайна, — а короткие потолки ставят на перишаговых производных контекстах, где истечение убивает только шаг.

### Проверка: Cancel на аллокаторе

**Вопрос:** Что вернёт chromedp.Cancel, если передать ей контекст, полученный из NewExecAllocator?

- nil — аллокатор закроется штатно
- Панику
- ✅ Ошибку ErrInvalidContext: у аллокатора нет chromedp-контекста
- Ошибку закрытия браузера CommandClose

Cancel работает с chromedp-контекстом из NewContext; у контекста аллокатора его нет, и Cancel возвращает ErrInvalidContext. Аллокатор закрывается собственной cancel-функцией, которая гасит все заведённые им браузеры. Для браузерных контекстов Cancel штатно закрывает браузер и возвращает ошибку уборки, если та случилась.

### Что сделает ретрай

```go
// act при каждой попытке возвращает ошибку,
// совместимую с context.DeadlineExceeded
// attempts = 3, стартовая пауза 1 секунда, удвоение
err := withRetry(ctx, 3, act)
```

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

- ✅ Три попытки с паузами 1с и 2с, затем ошибка «после 3 попыток»
- Одна попытка: первая же ошибка выходит из цикла
- Бесконечный цикл до успеха
- Три попытки без пауз: бэкофф работает только на сетевых ошибках

Обёртка делает ровно attempts попыток: первая сразу, затем пауза 1с, вторая, пауза 2с, третья — и возврат обёрнутой ошибки. DeadlineExceeded ретраится, Canceled оборвал бы цикл сразу, а удвоение паузы держит шторм повторов под контролем независимо от типа ошибки.

### Соберите перишаговый таймаут

Восстановите каркас: дедлайн на шаге, его освобождение и запуск действий.

```go
stepCtx, stepCancel := context.___(ctx, 30*time.Second)
___ stepCancel()
err := chromedp.___(stepCtx,
        chromedp.Navigate(url),
        chromedp.WaitVisible(".cards"),
)
```

Подсказка: Дедлайн ставит context.WithTimeout, освобождение планирует defer, а действия исполняет chromedp.Run.

Правильные ответы в пропусках: `WithTimeout`, `defer`, `Run`

### Расставьте каркас устойчивого шага

Соберите по порядку: контекст, прогрев, перишаговый дедлайн и шаг с логом ошибки.

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

1. `ctx, cancel := chromedp.NewContext(context.Background())`
2. `defer cancel()`
3. `if err := chromedp.Run(ctx, chromedp.Navigate("about:blank")); err != nil {`
4. `  log.Fatal(err)`
5. `}`
6. `stepCtx, stepCancel := context.WithTimeout(ctx, 30*time.Second)`
7. `defer stepCancel()`
8. `err := chromedp.Run(stepCtx, chromedp.Navigate(url))`
9. `if err != nil { log.Printf("%s: %v", url, err) }`

### Карточки: таймауты и устойчивость

- **context.WithTimeout** — Производный контекст с дедлайном: истёк — действия возвращают context.DeadlineExceeded. Отсчёт идёт от вызова WithTimeout, а не от начала Run; проверка ошибки — errors.Is.
- **Первый Run и таймаут** — Первый Run аллоцирует браузер; дедлайн на нём остановит весь браузер. Прогрев — Run без таймаута, короткие дедлайны — на перишаговых контекстах.
- **chromedp.Cancel** — Вежливая отмена: закрывает браузер штатно, дожидается уборки и возвращает ошибку. На контексте аллокатора — ErrInvalidContext.
- **context.Canceled** — Контекст отменили до дедлайна: сами — или chromedp при потере соединения с браузером. Ретраить бессмысленно — восстанавливать браузер.
- **Смерть браузера** — Соединение пропало — chromedp сам отменяет контекст, процесс гаснет. Восстановление: новый NewContext от того же аллокатора с теми же флагами.
- **Бэкофф** — Пауза между попытками растёт в геометрической прогрессии: 1с, 2с, 4с — шторм повторов гаснет сам. В библиотеке тот же приём — retryWithSleep внутри Query-механики.

Что дальше: устойчивость — последний слой под одиночным браузером, дальше курс раздваивается на масштаб и контроль. Шестнадцатый урок разберёт [вкладки и окна](/courses/chromedp/lessons/16-vkladki/): несколько страниц одного браузера и параллельные схемы поверх них. Двадцать второй соберёт сбои воедино — [ошибки и отладку](/courses/chromedp/lessons/22-oshibki-otladka/) chromedp: паники, режимы запуска, логи DevTools. Практика — локально: нужны установленный Go и Chrome, песочница сайта исполняет тематическую программу на чистом Go без браузера, и в ней честно не воспроизвести ни смерть Chrome, ни сетевые дедлайны — устойчивый парсер проверяется только на настоящем интернете и настоящем процессе. Домашняя проверка на честность: убейте процесс Chrome руками посреди обхода и прочитайте, что вернёт следующий Run, — эту ошибку вы теперь знаете в лицо.

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

- Дедлайн ставится контекстом: context.WithTimeout плюс defer cancel, а проверка ошибки — errors.Is, не сравнением строк.
- Первый Run аллоцирует браузер: таймаут на нём остановит браузер целиком — прогрев делают без дедлайна.
- Перишаговые дедлайны живут на производных контекстах: истёк шаг — браузер продолжает работать.
- Ретраятся таймауты и сетевые моргания; ErrPollingTimeout — повод чинить селектор, context.Canceled — сигнал без ретрая.
- Пауза ретрая растёт вдвое и уважает отмену родителя; ошибка оборачивается %w для errors.Is.
- При потере соединения chromedp сам отменяет контекст: context canceled без вашей вины — умер браузер.
- Восстановление — новый NewContext от живого аллокатора: свежий браузер с теми же флагами.
- chromedp.Cancel закрывает браузер штатно, дожидается уборки и возвращает ошибку; на аллокаторе — ErrInvalidContext.
- Дедлайны вкладываются: шаг меньше обхода, Poll-потолок конкурирует с ними — сработает тот, что раньше.

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

Соберите обход из пяти страниц на перишаговых таймаутах: прогрев без дедлайна, шаги по context.WithTimeout, лог ошибок с адресами. Затем уроните браузер — убейте процесс Chrome руками во время шага — и прочитайте ошибку следующего Run. Добавьте withRetry на адреса с таймаутами и измерьте, сколько попыток нужно при паузе 1с против 5с. Финал: поставьте общий потолок на весь обход и проверьте, что истечение общего дедлайна гасит шаги досрочно, но лог с адресами при этом доехал.

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

### Run вернул context canceled, хотя я не отменял контекст. Что это?

Браузер умер: при потере соединения chromedp сам отменяет контекст — процесс связан с ним, и соединение без браузера бессмысленно. Ретрай в этот контекст бессмыслен: он мёртв навсегда. Заведите новый контекст от того же аллокатора — это свежий браузер с теми же флагами, — и продолжайте обход с логом адреса, на котором оборвалась сессия.

### Какой таймаут ставить на шаг?

Измерьте, а не угадывайте: возьмите медианное время страницы и умножьте на запас — обычно 3-5 раз хватает. Слишком короткий потолок рождает ложные ретраи и лишнюю нагрузку на сайт, слишком длинный — ночные задания, которые висят на одной странице часами. Отдельно помните: дедлайн отсчитывается от WithTimeout, а не от начала Run, и включает ваши ожидания, чтения и оценки целиком.

### Почему нельзя просто обернуть весь обход в один большой WithTimeout?

Можно — общий потолок на задание это правильная практика. Опасно другое: если этот же контекст получает первый Run, дедлайн сгорает на аллокации и уносит весь браузер, а с ним и обход. Разделяйте: прогрев без дедлайна, шаги — короткими производными контекстами, общий потолок — поверх прогретого браузера, и следите, чтобы истечение общего дедлайна не совпало с медленным стартом на загруженной машине.
