20 вересня 2026 року сайт LIONEX нарешті почав рахувати власні HTTP-запити за маршрутами. Наступного дня ми вперше подивились на повну добу даних і побачили, що найпопулярніша адреса сайту — не головна і не блог. Нею виявився /api/health, службовий маршрут, якого жодна людина ніколи не відкриває. За добу він отримав 13 666 запитів, тобто 60 % усього трафіку застосунку.
Чому метрики запитів до 20 вересня не писались узагалі, ми розповідали в статті про чотири тривоги, яких не було. Тут — перша знахідка, яку ці метрики дали, два налаштування, що її прибрали, і спосіб перевірити те саме у себе за кілька хвилин.
Що показала перша доба
Health-маршрут існує для платформи, а не для відвідувачів. Docker, налаштований через Coolify, періодично стукає на нього й за відповіддю вирішує, чи живий контейнер. Кілька поганих відповідей поспіль — і контейнер перезапускається.
Інтервал цієї перевірки стояв 5 секунд. За налаштуваннями це 17 280 викликів на добу; метрики за 21 вересня записали 13 666, менше за розрахунок, але того самого порядку. Сама частота ще не біда: відповісти «процес живий» можна майже безкоштовно. Біда сиділа в коді маршруту. На кожен виклик він робив у базу SELECT 1, тож теоретично стільки ж, 17 280 запитів на добу, база отримувала лише для того, щоб підтвердити, що вона на місці.
Ця база стоїть на тому самому сервері, що й ще десяток наших проєктів. Того дня load average сервера сягнув 8,36 при порозі тривоги 8. Скажемо прямо: яку частку цього навантаження давав healthcheck, ми не міряли, і причиною його не називаємо. Бачили ми інше — на спільній машині цілодобово крутилась зайва робота, про яку ніхто не знав.
Чому ніхто не помічав
Окремий виклик тривав мілісекунди, помилок не давав і в логах не світився. Помітити його можна було лише одним способом: порахувати запити за маршрутами. До 20 вересня цього не робили, а без такого розподілу службова адреса може забирати більшість трафіку, і жоден графік цього не покаже.
Гірше те, що поодинці все виглядало розумно. П'ять секунд були значенням у панелі деплою, їх ніхто не обирав свідомо. Запит у базу колись дописали з добрим наміром, «щоб перевірка чесно дивилась на базу»: health, який бази не торкається, може називати здоровим контейнер, що вже не здатен віддати жодної сторінки. Кожне рішення окремо захищається. Разом вони давали запит у спільну базу кожні п'ять секунд, без кінця.
Два важелі, і потрібні обидва
Перший — інтервал. 21 вересня ми змінили його з 5 на 30 секунд. Кількість спроб лишили 10, тож до перезапуску контейнер тепер має провалювати перевірки приблизно 5 хвилин (10 × 30 с). Раніше вистачало 50 секунд (10 × 5 с).
У цього рішення є ціна, і її варто назвати: справжню відмову ми тепер помічаємо повільніше. Для сайту агенції без черги замовлень це прийнятно. Для платіжного сервісу, де кожна хвилина простою коштує грошей, — може й ні, і там ми зважували б інакше.
Другий важіль — сам маршрут. Успішний результат перевірки бази тепер пам'ятається 60 секунд у змінній процесу. Healthcheck отримує відповідь одразу, а база — щонайбільше один запит на хвилину.
Одного інтервалу було б замало: з ним кожен виклик і далі йшов би в базу, просто рідше. Один кеш прибрав би навантаження на базу, але платформа й надалі смикала б процес що п'ять секунд: 17 280 викликів на добу, кожен із запуском curl усередині контейнера. Разом вони розводять дві різні речі: як часто платформу цікавить стан і як часто відповідь справді доходить до бази.
Змінна процесу тут спрацювала тому, що сайт працює як довгоживучий сервер Node: процес стоїть годинами, і значення переживає між запитами. У serverless, де функція піднімається під кожен запит, такий кеш нічого б не запам'ятав.
Чому помилку не кешуємо
Пам'ятаємо лише успіх. Якщо база не відповіла, маршрут повертає 503 з текстом помилки, позначку успіху скидає, і наступний виклик знову йде в базу.
Логіка несиметрична навмисно. Успіх — звичайний стан, і його кешування знімає майже всю зайву роботу. Невдача — рівно той момент, коли за відповіддю платформа вирішує, чи перезапускати контейнер, тож кожна перевірка після неї має бути свіжою. Якби позначка успіху після збою лишалась, контейнер вважався б здоровим ще хвилину після падіння бази — саме тоді, коли його треба перезапускати.
Одна межа лишається й так: між останньою вдалою перевіркою і наступним справжнім запитом до бази минає до 60 секунд, і збій у цьому проміжку health побачить не одразу. На тлі п'ятихвилинного вікна до перезапуску ми вважаємо це прийнятним.
Перевірка
Спочатку локально: 13 викликів підряд дали один запит у базу, а виклик через 65 секунд знову пішов у базу. 22 вересня тести маршруту переписали: старий тест описував версію, якої давно не існувало, і взагалі не запускався, бо проєкту бракувало пакета jsdom. Нові перевіряють відповідь 200 і «ok», коли база на місці; відсутність другого запиту в базу протягом 60 секунд; 503 з текстом помилки, коли база недоступна; і те, що помилка не кешується.
Конфіг контейнера того ж дня: Interval 30 с, Timeout 5 с, Retries 10.
Цифри «після» і частка, яка майже не змінилась
22 вересня Prometheus за 15 хвилин поза вікном деплою показав 31,5 запиту до /api/health (дріб — наслідок того, як Prometheus рахує приріст лічильника на відрізку). Це близько 126 на годину, або приблизно 3 000 на добу замість 13 666.
Запитів у базу за побудовою кешу не більше одного на хвилину, тобто не більше 1 440 на добу замість 17 280. Ця цифра розрахована з налаштувань, окремо запити до бази ми не міряли.
Частка ж майже не зрушила. За ті самі 15 хвилин health дав 31,5 із 55 запитів застосунку, тобто досі більшість. Трафік сайту агенції невеликий, і службова адреса займає в ньому помітне місце за будь-якого інтервалу. Проблема ніколи не полягала в частці. Вона полягала в тому, що кожен із цих викликів ішов у спільну базу.
Пастка заміру: деплой у вікні
Перша спроба виміряти «після» дала 690 запитів за годину, наче ми нічого не змінили. Причина знайшлась у самому вікні: туди потрапив деплой нового образу. Під час розгортання платформа сама часто опитує health нового контейнера, а в системі метрик якийсь час живуть дві серії, старого й нового контейнера. За 15 хвилин після деплою вийшло 31,5.
Висновок простий: міряти поза вікном деплою, інакше легко «довести», що правка не спрацювала.
Як перевірити в себе
Подивіться інтервал. Для Docker:
docker inspect -f '{{json .Config.Healthcheck}}' <контейнер>
Interval там у наносекундах: 5000000000 — це 5 секунд. У Kubernetes те саме лежить у periodSeconds для liveness і readiness.
Прочитайте код health-маршруту. Що він робить на кожен виклик: іде в базу, у Redis, у зовнішній API? Помножте кожну таку дію на кількість викликів за добу:
викликів на добу = 86 400 / інтервал у секундах
86 400 / 5 = 17 280
Розведіть «процес живий» і «готовий обслуговувати». Kubernetes прямо розділяє liveness і readiness. У Docker перевірка одна, тому робочий компроміс такий: кешувати успіх і не кешувати помилку.
Хоч раз порахуйте запити за маршрутами — з логів проксі або з метрик застосунку. Без цього такі речі не видно в принципі.
Помножте Retries на Interval. Результат — скільки часу контейнер працює зламаним до перезапуску. Це число варто обрати свідомо, а не отримати в спадок від значень за замовчуванням.
Межі
Усе описане стосується одного сайту на одній платформі, Coolify з Docker. Цифри «до» взяті за одну добу, «після» — за 15 хвилин плюс конфіг контейнера. Кількість запитів до бази після правки розрахована, а не виміряна.
І головне обмеження: ми не стверджуємо, що сайт став швидшим або що навантаження сервера впало. Цього ми не міряли. Прибрали ми зайву постійну роботу, якої ніхто не бачив, і більше нічого.
Якщо хочете, щоб хтось переглянув healthcheck, тривоги й облік запитів на вашому сайті, це частина роботи з моніторингу доступності. Налаштування контейнерів і самого сервера входять в адміністрування сервера.
Для себе ми винесли одне правило: службовий маршрут теж трафік, і рахувати його треба так само, як сторінки для людей.