Почему у вас рассинхрон остатков между 1С и Ozon: 5 причин и как их закрывает retry-логика

Продали на Ozon — а в 1С товар всё ещё «в наличии». Следующий покупатель оформляет заказ, который отменять придётся вам. Разбираем по шагам, откуда берётся рассинхрон, во что он обходится и почему это не баг маркетплейса, а дыра в архитектуре интеграции.

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

Если у вас 20–30 SKU и один канал продаж, рассинхрон случается раз в месяц и не критичен. Но чем больше каталог и чем больше площадок (Ozon, Wildberries, собственный сайт, розница), тем чаще расходятся цифры — и тем дороже обходится каждая такая ошибка. Дальше разберём, откуда именно берётся расхождение и какая архитектура интеграции устраняет его системно, а не точечными правками.

Рассинхрон остатков — это не разовый сбой, а симптом. Система синхронизации либо не замечает свои ошибки, либо не умеет их исправлять сама. И то и другое — вопрос архитектуры, а не удачи.

Почему Ozon вообще так строго относится к остаткам

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

Пять причин, по которым 1С и Ozon расходятся в цифрах

⏱ Синхронизация по расписанию, а не по событию

Большинство обработок обновляют остатки раз в 5–15 минут по cron. Продажа, случившаяся между двумя запусками, просто не видна системе, пока не наступит следующий цикл — а за эти минуты успевает уйти ещё один заказ на тот же товар.

🔇 Ошибки API проглатываются молча

Запрос к Ozon Seller API отвалился по таймауту или вернул 500-ю ошибку — и обработка на встроенном языке 1С просто идёт дальше, без повторной попытки. Остаток, который должен был обновиться, остаётся старым до следующего запуска, а в логе — тишина.

🚦 Rate limit роняет запросы, а не откладывает их

У Ozon API есть лимиты на количество запросов в минуту. Без rate limiter часть batch-пакетов с ценами и остатками просто не отправляется при пиковой нагрузке — например, в момент массового обновления каталога.

🔀 Резервирование не атомарно между системами

Заказ пришёл одновременно с сайта и с Ozon на последнюю единицу товара. Если резервирование остатка в 1С и списание на маркетплейсе не связаны одной транзакцией, спишется дважды — а один из покупателей получит отмену.

✋ Ручные правки в кабинете Ozon в обход 1С

Менеджер поправил остаток прямо в личном кабинете Ozon, чтобы быстро закрыть проблему — и 1С, которая считает себя источником истины, на следующем цикле синхронизации перезатирает эту правку обратно.

Разбор причины № 1 подробнее: почему «раз в 5 минут» — это не «почти реальное время»

Интервал в 5 минут кажется небольшим, пока не посчитать через него оборот. Магазин с высоким спросом на топ-позиции может продавать одну и ту же популярную модель несколько раз за минуту в пиковые часы (распродажи, «чёрная пятница», утро понедельника после рекламной кампании). За 5 минут таких продаж может пройти пять-семь — и все они «увидят» один и тот же устаревший остаток, если синхронизация работает пакетно по расписанию, а не реагирует на само событие продажи.

Разбор причины № 4 подробнее: гонка запросов (race condition)

Это самая незаметная причина — её не видно в логах как «ошибку», потому что формально оба запроса прошли успешно. Проблема в порядке операций: если проверка остатка и его списание — это два раздельных шага, а не одна атомарная операция с блокировкой строки, между шагом «проверили — есть» и шагом «списали» может влезть второй параллельный запрос. В высоконагруженных системах это называют race condition, и лечится это либо пессимистической блокировкой на уровне БД, либо очередью, которая гарантирует последовательную обработку операций с одним и тем же товаром.

Во что обходится рассинхрон, если считать в деньгах

Каждая отменённая заявка — это не просто минус один заказ. Это: штраф маркетплейса за отмену по вине продавца, упущенная маржа с этой продажи, время менеджера на обработку отмены и возврат денег покупателю, и самое дорогое — просадка рейтинга продавца, которая снижает видимость всех остальных товаров в выдаче на недели вперёд. При объёме 500+ SKU и продажах на нескольких площадках даже 10–15 таких отмен в месяц складываются в сумму, сопоставимую с зарплатой отдельного сотрудника — подробный расчёт мы разбирали в отдельном материале о стоимости ручной синхронизации.

Как это закрывает retry-логика в открытом коннекторе

Мы столкнулись с этими же причинами при разработке open-source коннектора 1С ↔ Ozon и заложили обработку каждой из них на уровне архитектуры, а не «долатывания» руками после инцидента.

Exponential backoff вместо тишины

Неудачный запрос не пропадает — он уходит в очередь Symfony Messenger и повторяется с нарастающей паузой (1с → 2с → 4с...). Если Ozon временно недоступен, коннектор сам «дождётся» его восстановления, а не оставит остаток висеть до следующего cron.

📬 Асинхронная очередь вместо синхронного cron

Изменение остатка становится событием, которое сразу попадает в очередь на отправку, а не ждёт следующего планового запуска. Разрыв между «продали» и «обновили на маркетплейсе» сокращается с минут до секунд.

🧵 Rate limiter с batch-очередью

Запросы не роняются при превышении лимита, а выстраиваются в очередь и отправляются пачками до 1000 позиций за раз — ничего не теряется даже при массовом обновлении каталога.

📋 Dead-letter очередь для ручного разбора

Если запрос не прошёл даже после всех повторов — например, товар удалили на стороне Ozon — он не исчезает молча, а попадает в отдельную очередь для разбора, с полным логом причины.

А что с гонкой запросов и ручными правками?

Для race condition (причина № 4) коннектор использует блокировку на уровне записи при обработке остатка конкретного SKU — операции по одному и тому же товару выполняются строго последовательно, даже если события пришли почти одновременно. Для ручных правок в кабинете Ozon (причина № 5) единственное системное решение — договориться о том, что 1С является единственным источником истины, а любые изменения остатка в кабинете маркетплейса делать через интеграцию, а не в обход неё; коннектор при этом логирует каждое расхождение, которое обнаружил при сверке, чтобы такие случаи были видны, а не терялись.

Часто задаваемые вопросы

Как часто должна происходить синхронизация остатков 1С и Ozon?

Для каталога с высоким оборотом — по событию, то есть сразу при изменении остатка в 1С или получении заказа с Ozon, без ожидания планового запуска. Для низкооборотных товаров периодическая синхронизация раз в несколько минут может быть достаточной, но важно, чтобы у системы была возможность обработать оба сценария.

Можно ли обойтись без отдельного сервера для синхронизации?

Да, если объём небольшой — обработка на встроенном языке 1С по регламентному заданию справится. Отдельный сервис с очередью сообщений становится оправданным при росте каталога и частоты обновлений — подробно сравнение архитектур мы разбираем в отдельной статье.

Что делать, если Ozon API недоступен несколько часов?

Правильная архитектура не теряет события за время недоступности — они копятся в очереди и обрабатываются автоматически, как только API снова отвечает. Без очереди события, произошедшие во время простоя API, приходится искать и досинхронизировать вручную.

Решает ли эту проблему просто более частый запуск cron?

Частично и ненадолго. Уменьшение интервала с 15 до 5 минут снижает окно рассинхрона, но не устраняет причину — ошибки по-прежнему теряются молча, гонка запросов никуда не девается, а нагрузка на API и на саму 1С растёт пропорционально частоте запуска.

Итог

Retry-логика и очереди — это не про «красивый код». Это про то, что каждая неудачная попытка синхронизации становится видимой и обрабатываемой, а не тихо теряется до следующего цикла. Для магазина с сотнями SKU разница между «синхронизация раз в 10 минут, ошибки молча теряются» и «синхронизация по событию с повторными попытками» — это разница между регулярными штрафами за рассинхрон и их полным отсутствием.

Код коннектора открыт — можно посмотреть, как именно устроены retry, очереди и rate limiter, ещё до того, как принимать решение о внедрении.