WordPress может иметь быстрый PHP, оптимизированную базу данных и нормальный page cache — и всё равно периодически отвечать по 3–8 секунд. Частая причина, которую легко пропустить: внешний HTTP-запрос выполняется прямо внутри пользовательского запроса и WordPress ждёт чужой сервер.
Так работают интеграции с CRM, курсами валют, доставкой, лицензиями, API аналитики, внешними каталогами, webhook-сервисами и другими системами. Если удалённый endpoint отвечает медленно или зависает, задержка попадает в TTFB страницы, AJAX-запроса, REST API или checkout.
Ниже — практическая схема: как найти такие вызовы, понять, какие из них действительно блокируют ответ, и перенести некритичные операции из пути пользователя.
Почему внешний API может тормозить WordPress сильнее базы данных
WordPress HTTP API предоставляет функции wp_remote_get(), wp_remote_post() и wp_remote_request(). Внутри они используют WP_Http. По официальной документации стандартный HTTP-запрос имеет timeout 5 секунд, а параметр blocking по умолчанию равен true.
Если код страницы вызывает внешний endpoint и ему нужен ответ, выполнение PHP ждёт сеть. Один запрос на 2,4 секунды способен добавить примерно те же 2,4 секунды к текущему PHP-request. Несколько последовательных вызовов складываются.
Особенно неприятны нестабильные API: сегодня ответ приходит за 120 мс, завтра DNS, TLS или сам сервис задерживают запрос на несколько секунд. Поэтому проблема часто выглядит как «сайт иногда тормозит», хотя MySQL и CPU почти свободны.
Где искать внешние запросы
| Источник | Типичный пример | Риск |
|---|---|---|
| Плагин | Проверка лицензии, обновлений, статистики | Вызов на каждом admin/frontend request |
| CRM / ERP | Получение статуса клиента или цены | Удалённая система становится частью TTFB |
| WooCommerce | Доставка, платёж, антифрод, склад | Тормозит cart/checkout/AJAX |
| Внешний контент | Курсы, отзывы, остатки, каталог | Одинаковые данные скачиваются для каждого посетителя |
| Webhook | Отправка события в CRM или бот | Страница ждёт подтверждения, хотя оно не нужно пользователю |
| Тема / кастомный код | wp_remote_get() в init, shortcode или шаблоне |
Запрос повторяется на большом числе страниц |
Сначала отделите сеть от SQL и PHP
До изменения timeout важно доказать, что задержка действительно сетевая. Если страница медленная из-за сотен SQL-запросов, уменьшение HTTP timeout ничего не исправит.
Для общей диагностики удобно начать с Query Monitor и разбора медленных запросов WordPress. Он помогает увидеть SQL, PHP-ошибки, hooks, REST/AJAX-контекст и часть HTTP-вызовов. Затем подозрительный endpoint измеряют отдельно.
Как измерить wp_remote_get в своём коде
Если вызов находится в вашем плагине или теме, замерьте его непосредственно вокруг HTTP API. В лог не нужно писать токены, Authorization и полный query string.
function asg_timed_remote_get( string $url, array $args = [] ) {
$started = microtime( true );
$response = wp_safe_remote_get( $url, $args );
$elapsed_ms = (int) round( ( microtime( true ) - $started ) * 1000 );
$host = (string) wp_parse_url( $url, PHP_URL_HOST );
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
$result = is_wp_error( $response )
? $response->get_error_code()
: (string) wp_remote_retrieve_response_code( $response );
error_log( sprintf( '[ext-http] host=%s path=%s ms=%d result=%s', $host, $path, $elapsed_ms, $result ) );
return $response;
}
Такой wrapper полезен для временной диагностики собственного кода. После нахождения проблемы постоянный подробный лог лучше убрать или ограничить, чтобы не создавать лишний I/O.
Как ловить HTTP-запросы чужих плагинов
Для запросов, которые выполняет неизвестный плагин, есть системный hook http_api_debug. WordPress вызывает его после получения HTTP-ответа и до возврата результата вызывающему коду.
add_action( 'http_api_debug', static function ( $response, $context, $class, $parsed_args, $url ) {
$host = (string) wp_parse_url( $url, PHP_URL_HOST );
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
$result = is_wp_error( $response )
? $response->get_error_code()
: (string) wp_remote_retrieve_response_code( $response );
error_log( sprintf( '[wp-http] host=%s path=%s result=%s', $host, $path, $result ) );
}, 10, 5 );
Сам http_api_debug не сообщает длительность. Для времени можно временно сопоставить старт через http_request_args или использовать профилировщик. Главное — не превращать production-лог в дамп заголовков и секретов.
Не логируйте полный URL, если в нём есть секреты
Некоторые API передают ключ в query string. Если записывать весь URL в debug.log, секрет попадёт в файл, backup и, возможно, систему сбора логов. Безопаснее логировать hostname, path, длительность, HTTP-код и технический error code.
Timeout нужно задавать по смыслу операции
Глобальное увеличение timeout — плохая «оптимизация». Если внешний сервис уже отвечает плохо, переход с 5 до 30 секунд делает зависание заметно хуже.
$response = wp_safe_remote_get(
'https://api.example.com/rates',
[
'timeout' => 2.0,
'redirection' => 2,
]
);
Универсального числа нет. Платёжная операция, доставка и fire-and-forget аналитика имеют разные требования. Timeout выбирают отдельно для интеграции и обязательно обрабатывают WP_Error.
Кэшируйте внешние данные, которые не меняются каждую секунду
Официальный WordPress handbook рекомендует рассматривать кэш для внешних API: иначе каждый посетитель снова ждёт удалённый сервис.
$cache_key = 'asg_external_rates_v1';
$data = get_transient( $cache_key );
if ( false === $data ) {
$response = wp_safe_remote_get( 'https://api.example.com/rates', [ 'timeout' => 2.0 ] );
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( is_array( $data ) ) {
set_transient( $cache_key, $data, 10 * MINUTE_IN_SECONDS );
}
}
}
TTL зависит от данных. Остатки склада могут требовать минут, справочник городов — часов или суток. Если установлен persistent object cache, transient может обслуживаться ещё быстрее, но срок актуальности всё равно надо проектировать.
Stale fallback лучше белого экрана
Для многих информационных API допустимо показать последнее корректное значение, если свежий запрос временно упал. Практическая схема — хранить fresh cache с коротким TTL и отдельно последнее успешное значение дольше. При ошибке пользователь получает stale data, а обновление повторяется позже.
Такой подход нельзя автоматически переносить на платежи, авторизацию или критичную проверку остатков: там устаревшие данные могут быть опаснее ошибки.
Не вызывайте внешний API в каждом init или wp_head
Код в init, wp_head, shortcode, product loop или шаблоне исполняется намного чаще, чем кажется. Один HTTP-запрос, добавленный «просто получить статус», начинает выполняться на каждой странице.
- Нужен ли ответ именно для текущей страницы?
- Можно ли использовать локально сохранённое значение?
- Можно ли обновить данные асинхронно и показать готовый cache?
Когда можно использовать blocking=false
WP_Http::request() поддерживает blocking => false. WordPress отправляет запрос и возвращает управление без ожидания обычного response body.
wp_remote_post(
'https://hooks.example.com/event',
[
'timeout' => 1,
'blocking' => false,
'body' => [ 'event' => 'page_view' ],
]
);
Это подходит только когда результат не нужен пользователю и потеря единичного события допустима либо есть другой механизм доставки. Не используйте fire-and-forget для оплаты, создания заказа во внешней ERP или списания склада. Для критичных операций нужна очередь с retry и статусом.
Критичные интеграции лучше выносить в очередь
Если после оформления заказа нужно отправить данные в CRM, склад, бухгалтерию и уведомления, не обязательно заставлять checkout ждать все системы по очереди.
- Локально сохранить заказ и критичные данные.
- Поставить задачу в очередь.
- Вернуть пользователю локально подтверждённый результат.
- Worker выполняет внешний запрос.
- При временной ошибке делает retry с backoff.
- После лимита попыток оставляет понятный failed status для оператора.
В WooCommerce для фоновых задач часто используют Action Scheduler. В обычном WordPress можно использовать WP-Cron или отдельный real cron/worker. Но перенос в cron сам по себе не даёт надёжность: нужны idempotency, retry и журнал результата.
Polling часто можно заменить webhook
Если WordPress каждые 30 секунд спрашивает CRM «что изменилось?», сайт создаёт постоянную сетевую нагрузку. Если сервис поддерживает webhook, удалённая система может сама уведомлять WordPress о событии.
Если данные забираются пачками через REST API, дополнительно проверьте правильную пагинацию WordPress REST API без пропусков и лишней нагрузки.
Retry нужен не для каждой ошибки
Временный DNS/timeout, HTTP 429, 502, 503 иногда имеет смысл повторить с backoff. Но 400 из-за неверных параметров или 401 из-за авторизации не исправятся от десяти повторов подряд. Не выполняйте пять retry синхронно в том же page request: один timeout превратится в многосекундное зависание.
Добавьте circuit breaker для нестабильного сервиса
Если endpoint несколько раз подряд не отвечает, нет смысла заставлять каждого посетителя снова ждать его timeout. Circuit breaker временно помечает интеграцию недоступной, использует fallback и разрешает редкие контрольные попытки. После успешного ответа обычная работа возобновляется.
pre_http_request полезен для тестов и точечного блокирования
Hook pre_http_request позволяет short-circuit WordPress HTTP API: вернуть подготовленный response или WP_Error до реального сетевого вызова. Это удобно на staging и в automated tests, чтобы не отправлять реальные запросы в платёжный или CRM API.
Глобальный запрет всех внешних запросов опасен: WordPress core и плагины используют сеть для обновлений и других функций. Блокирование должно быть адресным.
Пользовательский URL — только через безопасный HTTP API
Документация wp_remote_get() отдельно предупреждает: если URL контролируется пользователем, используйте wp_safe_remote_get(). Дополнительно полезен allowlist допустимых hostnames, если бизнес-логика знает, к каким сервисам разрешено подключаться.
Почему CDN не исправляет медленный внешний API внутри WordPress
Page cache и CDN могут скрыть проблему на публичных страницах. Но checkout, кабинет, REST/AJAX и персонализированные запросы всё равно идут в PHP. Если внутри PHP находится блокирующий API-вызов, задержка возвращается.
Общую схему безопасного page cache я разбирал в материале про кеширование WordPress без поломки форм.
Практический чек-лист диагностики
- Сравнить TTFB быстрой и медленной страницы.
- Проверить SQL/PHP через Query Monitor или профилировщик.
- Найти WordPress HTTP API вызовы на проблемном request.
- Записать hostname, path, длительность, HTTP-код или WP_Error.
- Не логировать Authorization, cookies, API keys и query secrets.
- Проверить, нужен ли внешний response пользователю прямо сейчас.
- Для справочных данных добавить cache и stale fallback.
- Для некритичных событий — фон или очередь.
- Для критичных интеграций — очередь, idempotency, retry и статус.
- Уменьшить timeout адресно, а не глобально.
- Проверить checkout, AJAX, REST API, cron и админку отдельно.
- После исправления отключить временную подробную диагностику.
Пример архитектуры без блокировки страницы
Допустим, карточка товара показывает рейтинг поставщика из внешней системы. Плохая схема: при каждом открытии выполнять wp_remote_get() и ждать API. Лучше: cron или queue обновляет рейтинг раз в 10 минут, сохраняет локально, а карточка читает готовое значение.
Для CRM похожий принцип: форма сначала надёжно сохраняет заявку локально, затем фоновая задача отправляет её наружу. Если CRM упала, заявку можно повторно отправить, а не потерять вместе с timeout пользователя.
Когда нужен технический аудит
Если непонятно, какой плагин инициирует запросы, задержка появляется только под нагрузкой или проблема затрагивает WooCommerce checkout, лучше смотреть весь request trace: PHP, SQL, HTTP, cron, Action Scheduler и внешние сервисы вместе.
A.S Groups может провести диагностику WordPress/WooCommerce, найти блокирующие внешние вызовы, настроить кэш, timeout, очередь и безопасный fallback без отключения нужных интеграций. Для интеграционных проектов также полезна страница CRM-интеграции с сайтом.
Официальные документы WordPress
- WordPress HTTP API — Making HTTP requests
- HTTP API — Performance и caching
- WP_Http::request(): timeout и blocking
- Hook http_api_debug
- Hook pre_http_request
- wp_remote_get() и wp_safe_remote_get()
Главный вывод: внешний API не должен автоматически становиться частью времени загрузки каждой страницы. Если ответ не нужен пользователю немедленно — его обычно выгоднее кэшировать, обновлять фоном или обрабатывать через очередь.
Обсуждение
Вопросы и комментарии
Можно уточнить детали статьи или поделиться своим опытом. Первый комментарий проходит проверку.