{"id":48455360,"url":"https://github.com/wmakeev/moysklad-fetch-planner","last_synced_at":"2026-04-06T22:31:06.928Z","repository":{"id":40702889,"uuid":"380796025","full_name":"wmakeev/moysklad-fetch-planner","owner":"wmakeev","description":"Планировщик запросов для API МойСклад","archived":false,"fork":false,"pushed_at":"2025-10-06T06:51:22.000Z","size":493,"stargazers_count":4,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-06T08:58:08.766Z","etag":null,"topics":["fetch","fetch-api","moysklad","planner","rate-limit"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wmakeev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-27T17:08:08.000Z","updated_at":"2025-10-06T06:51:27.000Z","dependencies_parsed_at":"2025-02-06T15:25:33.114Z","dependency_job_id":"a1136a59-7b9d-4349-8d5f-f726a6eaf45f","html_url":"https://github.com/wmakeev/moysklad-fetch-planner","commit_stats":{"total_commits":2,"total_committers":1,"mean_commits":2.0,"dds":0.0,"last_synced_commit":"50b364a891fa800d46de410575408583e5e81699"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/wmakeev/moysklad-fetch-planner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmakeev%2Fmoysklad-fetch-planner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmakeev%2Fmoysklad-fetch-planner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmakeev%2Fmoysklad-fetch-planner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmakeev%2Fmoysklad-fetch-planner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wmakeev","download_url":"https://codeload.github.com/wmakeev/moysklad-fetch-planner/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmakeev%2Fmoysklad-fetch-planner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31492751,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T17:22:55.647Z","status":"ssl_error","status_checked_at":"2026-04-06T17:22:54.741Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fetch","fetch-api","moysklad","planner","rate-limit"],"created_at":"2026-04-06T22:31:06.820Z","updated_at":"2026-04-06T22:31:06.912Z","avatar_url":"https://github.com/wmakeev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# moysklad-fetch-planner \u003c!-- omit in toc --\u003e\n\n[![npm](https://img.shields.io/npm/v/moysklad-fetch-planner.svg?cacheSeconds=1800\u0026style=flat-square)](https://www.npmjs.com/package/moysklad-fetch-planner)\n[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/wmakeev/moysklad-fetch-planner/main.yml?style=flat-square)](https://github.com/wmakeev/moysklad-fetch-planner/actions/workflows/main.yml)\n[![Codecov](https://img.shields.io/codecov/c/github/wmakeev/moysklad-fetch-planner?style=flat-square)](https://app.codecov.io/gh/wmakeev/moysklad-fetch-planner/tree/master/)\n![no dependencies](https://img.shields.io/badge/dependencies-no-green?style=flat-square)\n\n\u003e Планировщик запросов для API МойСклад\n\nПозволяет выполнять асинхронные запросы к МойСклад без опасения получить ошибку превышения лимитов - `429 Too Many Requests`.\n\nЧтобы уложиться в доступные лимиты полученные из заголовков ответа сервера (напр. `X-RateLimit-Limit` и `X-RateLimit-Remaining`), планировщик автоматически вносит задержку между выполнением отдельных запросов, а так же регулирует максимальное кол-во параллельных запросов. Прозрачно выполняет повтор запроса если ошибки избежать не удалось.\n\nТакой подход позволяет обеспечить быстрое выполнение запросов с минимальной задержкой в моменте и стабильную работу при длительной высокой нагрузке, в том числе, при наличии задач в других процессах, разделяющих с вашим приложением общий лимит.\n\n## Содержание \u003c!-- omit in toc --\u003e\n\n- [Быстрый старт](#быстрый-старт)\n  - [С использованием fetch](#с-использованием-fetch)\n  - [С использованием библиотеки moysklad](#с-использованием-библиотеки-moysklad)\n- [Принцип работы](#принцип-работы)\n  - [Параллельные запросы](#параллельные-запросы)\n- [Конфигурация](#конфигурация)\n  - [Обработчик событий `eventHandler`](#обработчик-событий-eventhandler)\n  - [Коэффициент тротлинга `throttlingCoefficient`](#коэффициент-тротлинга-throttlingcoefficient)\n    - [Рекомендации](#рекомендации)\n  - [Максимальное кол-во параллельных запросов `maxParallelLimit`](#максимальное-кол-во-параллельных-запросов-maxparallellimit)\n  - [Максимальное время задержки между запросами `maxRequestDelayTimeMs`](#максимальное-время-задержки-между-запросами-maxrequestdelaytimems)\n  - [Jitter `jitter`](#jitter-jitter)\n  - [Период пересмотра параллельных запросов `parallelLimitCorrectionPeriodMs`](#период-пересмотра-параллельных-запросов-parallellimitcorrectionperiodms)\n- [Использование](#использование)\n  - [Создание экземпляра планировщика](#создание-экземпляра-планировщика)\n  - [wrapFetch(fetch, options)](#wrapfetchfetch-options)\n- [Методы FetchPlanner](#методы-fetchplanner)\n  - [getFetch()](#getfetch)\n  - [waitForFreeRequestSlot(priority)](#waitforfreerequestslotpriority)\n  - [getOptions()](#getoptions)\n  - [getRateLimit()](#getratelimit)\n  - [getRateLimitRemaining()](#getratelimitremaining)\n  - [getActionsQueueLength()](#getactionsqueuelength)\n  - [getRequestSlotHandlersCount()](#getrequestslothandlerscount)\n  - [getParallelLimitCorrection()](#getparallellimitcorrection)\n  - [getCurInflightRequestsCount()](#getcurinflightrequestscount)\n  - [getLastRequestDelay()](#getlastrequestdelay)\n  - [getNextRequestTime()](#getnextrequesttime)\n- [События](#события)\n  - [Подписка на события](#подписка-на-события)\n  - [Виды событий](#виды-событий)\n    - [`request` - отправка запроса](#request---отправка-запроса)\n    - [`response` - получение ответа](#response---получение-ответа)\n    - [`fetch-error` - ошибка при попытке выполнить запрос](#fetch-error---ошибка-при-попытке-выполнить-запрос)\n- [Пример: Разделение лимита между разными процессами](#пример-разделение-лимита-между-разными-процессами)\n\n## Быстрый старт\n\n### С использованием fetch\n\n```ts\nimport { wrapFetch } from 'moysklad-fetch-planner'\n\nconst wrappedFetch = wrapFetch(fetch)\n\nconst result = await wrappedFetch(\n  'https://api.moysklad.ru/api/remap/1.2/entity/customerorder?limit=100',\n  {\n    headers: {\n      'Authorization': '...'\n      'Accept-Encoding': 'gzip'\n    }\n  }\n)\n\nconsole.log(`Номер первого заказа - ${result.rows[0].name}`)\n```\n\n### С использованием библиотеки [moysklad](https://github.com/wmakeev/moysklad)\n\n```ts\nimport Moysklad from 'moysklad'\nimport { wrapFetch } from 'moysklad-fetch-planner'\n\n// Логин и пароль указаны в переменных окружения\nconst ms = Moysklad({ fetch: wrapFetch(fetch) })\n\nconst result = await ms.GET('entity/customerorder', { limit: 100 })\n\nconsole.log(`Номер первого заказа - ${result.rows[0].name}`)\n```\n\n## Принцип работы\n\nПри создании экземпляра планировщика передается объект с интерфейсом [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).\n\n\u003e Начиная с версии Node.js 18+ [fetch](https://nodejs.org/docs/latest/api/globals.html#fetch) входит в стандартную библиотеку и доступен в глобальной области видимости.\n\n```ts\nconst fetchPlanner = new FetchPlanner(fetch)\n```\n\nСоздается прокси-метод для fetch с тем же интерфейсом, но с поддержкой планирования запросов к API МойСклад.\n\n```ts\nconst wrappedFetch = fetchPlanner.getFetch()\n```\n\nВсе запросы отправленные через обернутый fetch попадают в планировщик и добавляются в очередь. При наличия свободного лимита, запрос извлекается из очереди и выполняется.\n\nТекущий доступный лимит определяется из соответствующих [HTTP заголовков](https://dev.moysklad.ru/doc/api/remap/1.2/index.html#mojsklad-json-api-obschie-swedeniq-obrabotka-oshibok:~:text=%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D1%85%20%D0%B7%D0%B0%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D0%BA%D0%BE%D0%B2-,X%2DRateLimit%2DLimit,-%2D%20%D0%BA%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%BE%20%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2%2C%20%D0%BA%D0%BE%D1%82%D0%BE%D1%80%D1%8B%D0%B5) в ответах сервера API МойСклад.\n\nПланировщик автоматически рассчитывает задержку для каждого отдельного запроса которая нужна для того, чтобы не превышать указанные лимиты.\n\n\u003e Первые запросы отправленные в планировщик, вплоть до получения первого ответа в заголовках которого содержится информация о лимитах, выполняются без задержки.\n\nЗадержка динамически меняется в зависимости от размера доступного лимита. Если лимит близок к 100%, то задержка минимальна и начинает расти с уменьшением лимита, достигая [максимального значения](#максимальное-время-задержки-между-запросами-maxrequestdelaytimems) при 0% от лимита.\n\nВ случае, если ошибка `429 Too Many Requests` всё же произошла, планировщик возвращает запрос обратно в начало очереди и повторяет его в штатном режиме согласно описанной логике.\n\n### Параллельные запросы\n\nПомимо лимита на кол-во запросов за единицу времени (RateLimit), МойСклад имеет лимит на кол-во параллельных запросов. Текущее кол-во выполняемых параллельных запросов не возвращается в ответе сервера API МойСклад. Поэтому планировщик может сделать вывод о превышении этого лимита только по факту получения ошибки `429 Too Many Requests` со значением `429005` в заголовке `X-Lognex-Auth`.\n\nПри каждой такой ошибке, максимальное допустимое кол-во параллельных запросов снижается на единицу вплоть до `1`.\n\nЧерез [определенное время](#период-пересмотра-параллельных-запросов-parallellimitcorrectionperiodms) происходит пересмотр значения максимального значения параллельных запросов. И если за это время ошибка не повторялась, то значение снова увеличивается на `1`, постепенно восстанавливаясь до [максимума](#максимальное-кол-во-параллельных-запросов-maxparallellimit).\n\n## Конфигурация\n\nДля тонкой настройки планировщика можно передавать опции:\n\n```ts\nconst wrappedFetch = wrapFetch(fetch, options)\n```\n\nили\n\n```ts\nconst fetchPlanner = new FetchPlanner(fetch, options)\n```\n\nПараметры `options` (все опциональны):\n\n### Обработчик событий `eventHandler`\n\nДолжен соответствовать следующему минимальному интерфейсу:\n\n```typescript\ninterface EventHandler {\n  emit(eventName: string, data?: any): void\n}\n```\n\nМожно использовать для логирования, сбора статистики и анализа нагрузки\nна API.\n\n[Подробнее в разделе \"События\"](#события)\n\n### Коэффициент тротлинга `throttlingCoefficient`\n\n**По умолчанию:** `5` (допускается значение от 1 до 20)\n\nКоэффициент регулирующий интенсивность тротлинга запросов в зависимости от доступных в текущий момент лимитов.\n\n\u003e Процент оставшегося лимита запросов можно рассчитать как отношение значений в заголовках ответа API `X-RateLimit-Remaining` к `X-RateLimit-Limit`.\n\nПри минимальном значении коэффициента `throttlingCoefficient` равном `1` задержка нарастает линейно от `0` мс при 100% лимита до `maxRequestDelayTimeMs` мс при 0% лимита. При значении коэффициента больше `1`, задержка начинает нарастать нелинейно, уменьшая задержку при лимитах близких к 100% и увеличивая при уменьшении лимита. Например если коэффициент равен `10`, то тротлинга практически не будет вплоть до ~30% от лимита, а далее начнется резкое увеличение задержки до `maxRequestDelayTimeMs`.\n\nНа иллюстрации пример зависимости размера задержки от лимита при различных коэффициентах:\n\n![Зависимость задержки от лимита](./resource/coefficient-and-delay.svg)\n\n#### Рекомендации\n\n- Если известно, что приложение будет работать в окружении других параллельных задач, разделяющих между собой лимит запросов, то имеет смысл установить меньший коэффициент в диапазоне от 1 до 4.\n\n- Если приложение самостоятельно разделяет доступные лимиты и важна максимальная производительность для серий последовательных запросов, когда задержка минимальна в первые секунды и начинает нарастать уже при сохранении нагрузки, то можно увеличивать коэффициент.\n\nВажно понимать, что, вне зависимости от размера коэффициента, при постоянной интенсивной нагрузке на API задержка всегда выходит на одно и то же среднее значение примерно равное отношению `X-Lognex-Retry-TimeInterval` / `X-RateLimit-Limit` (~66 мс).\n\n### Максимальное кол-во параллельных запросов `maxParallelLimit`\n\n**По умолчанию:** `4` (максимальное значение для МойСклад `5`)\n\nAPI МойСклад позволяет до 5 параллельных запросов, но для большей надежности лучше оставлять небольшой запас. Планировщик умеет определять ситуации, когда приложение сталкивается с лимитом на кол-во параллельных запросов, постепенно уменьшая указанный лимит. Но если на одном аккаунте работает много приложений или приложение интенсивно использует API, то лучше сразу явно ограничить кол-во параллельных запросов до 2-3 или даже меньше.\n\n### Максимальное время задержки между запросами `maxRequestDelayTimeMs`\n\n**По умолчанию:** `3000` (3 секунды)\n\nМаксимальное время задержки перед выполнением следующего запроса.\n\nУстанавливается в случае когда оставшийся лимит равен нулю (либо произошла ошибка превышения лимита на кол-во запросов в единицу времени).\n\nТакая ситуация может произойти если:\n\n- работает слишком много параллельных приложений\n- какое-либо параллельное приложение не контролирует лимиты\n\nЗадержка между запросами всегда лежит в диапазоне от 0 до `maxRequestDelayTimeMs`.\n\n### Jitter `jitter`\n\n**По умолчанию** `0.1` (максимальное значение `0.3`)\n\nДоля от значения `maxRequestDelayTimeMs` в пределах которой будет случайным образом изменено значение `maxRequestDelayTimeMs` при расчете величины задержки между запросами.\n\nУказание jitter'a (дрожания) способствует более равномерному распределению запросов при одновременной параллельной работе нескольких приложений.\n\nТак же jitter применяется для расчета времени [коррекции кол-ва параллельных запросов](#период-пересмотра-параллельных-запросов-parallellimitcorrectionperiodms).\n\n### Период пересмотра параллельных запросов `parallelLimitCorrectionPeriodMs`\n\n**По умолчанию:** `10000` (10 секунд)\n\nПериод через которые происходим пересмотр допустимого кол-ва параллельных запросов.\n\nЛимит уменьшается при каждой ошибке превышения кол-ва параллельных запросов с шагом `1`\n\nЛимит постепенно увеличивается снова до `maxParallelLimit` с шагом `1` через указанный промежуток времени.\n\n## Использование\n\nКаждый отдельный экземпляр планировщика имеет независимую очередь запросов.\n\n### Создание экземпляра планировщика\n\n```js\nimport { FetchPlanner } from 'moysklad-fetch-planner'\n\nconst fetchPlanner = new FetchPlanner(fetch, {\n  maxRequestDelayTimeMs: 1000\n})\n\nconst wrappedFetch = fetchPlanner.getFetch()\n\nconst authHeader = 'Basic ' + Buffer.from(\n  `${process.env.MOYSKLAD_LOGIN}:${process.env.MOYSKLAD_PASSWORD}`\n).toString('base64')\n\nconst result = await wrappedFetch(\n  'https://api.moysklad.ru/api/remap/1.2/customerorder?limit=100',\n  {\n    headers: {\n      'Authorization': authHeader\n      'Accept-Encoding': 'gzip'\n    }\n  }\n)\n\nconsole.log(`Номер первого заказа - ${result.rows[0].name}`)\n```\n\n### wrapFetch(fetch, options)\n\nСокращенный вариант - передать fetch в функцию wrapFetch, которая создаст экземпляр планировщика и вернет обернутый fetch.\n\n**Параметры:**\n\n- `fetch` - функция `fetch` с интерфейсом FetchAPI\n- `options` - параметры [конфигурации](#конфигурация)\n\n```js\nimport { wrapFetch } from 'moysklad-fetch-planner'\n\nconst wrappedFetch = wrapFetch(fetch, {\n  maxRequestDelayTimeMs: 1000\n})\n\n// ... см. пример выше\n```\n\n## Методы FetchPlanner\n\n### getFetch()\n\nВозвращает обернутый fetch для выполнения запросов через планировщик. Исходный fetch передается в параметрах конструктора при создании экземпляра FetchPlanner.\n\n### waitForFreeRequestSlot(priority)\n\nОжидание фактического наличия свободного слота для выполнения запроса.\n\nПараметры:\n\n- `priority` - Приоритет (`number`). Чем меньше число, тем раньше будет выделен слот (по умолчанию - `0`)\n\nВ примере ниже в цикле выполнятся сразу 100 запросов.\n\n- Если поставить `await` перед `ms.GET(...)`, то все запросы будут выполнятся последовательно, что не подходит для ситуации когда задача выполнить запросы параллельно.\n\n- Если убрать строку `await fetchPlanner.waitForFreeRequestSlot()`, то все 100 запросов сразу попадут в очередь планировщика, что не будет хорошим решением при большом кол-ве запросов.\n\n- В данном случае с использованием `await fetchPlanner.waitForFreeRequestSlot()` запросы будут выполнятся параллельно, в то же время, не будут забивать очередь планировщика. И мы можем контролировать поток выполнения внутри цикла.\n\n```js\nconst resultsPromises = []\n\nfor (let i = 1; i \u003c 100; i++) {\n  await fetchPlanner.waitForFreeRequestSlot()\n\n  const resultsPromise = ms.GET('entity/product', {\n    offset: Math.round(Math.random() * 100) + 1,\n    limit: 1\n  })\n\n  resultsPromises.push(resultsPromise)\n}\n\nconst results = await Promise.all(resultsPromises)\n```\n\n### getOptions()\n\nВозвращает параметры планировщика по умолчанию либо указанные при его создании.\n\n### getRateLimit()\n\nВозвращает текущее актуальное значение заголовка `X-RateLimit-Limit`.\n\n### getRateLimitRemaining()\n\nВозвращает текущее актуальное значение заголовка `X-RateLimit-Remaining`.\n\n### getActionsQueueLength()\n\nВозвращает текущую длину очереди запросов.\n\n### getRequestSlotHandlersCount()\n\nВозвращает количество обработчиков ожидающих свободного слота.\n\n### getParallelLimitCorrection()\n\nВозвращает текущую коррекцию кол-ва допустимых параллельных запросов.\n\n### getCurInflightRequestsCount()\n\nВозвращает текущее кол-во незавершенных запросов (ожидающих ответа сервера)\n\n### getLastRequestDelay()\n\nВозвращает задержку последнего запроса (ms)\n\n### getNextRequestTime()\n\nВозвращает время мс (Unix Timestamp) на которое запланирован следующий запрос или `0`, если нет запланированных запросов.\n\n## События\n\n### Подписка на события\n\nПример подписки на события планировщика:\n\n```js\nimport { EventEmitter } from 'node:events'\nimport { FetchPlanner, FetchPlannerEventMap } from 'moysklad-fetch-planner'\n\nconst eventHandler = new EventEmitter\u003cFetchPlannerEventMap\u003e()\n\neventHandler.on('request', function (ev, planner) {\n  console.log(ev.actionId, planner.getRateLimit())\n})\n\neventHandler.on('response', (ev, planner) =\u003e {\n  console.log(ev.actionId, planner.getRateLimit())\n})\n\nconst fetchPlanner = new FetchPlanner(fetch, {\n  eventHandler\n})\n```\n\n### Виды событий\n\n#### `request` - отправка запроса\n\nСодержит поля:\n\n| Наименование | Тип                              | Описание                                                                                                          |\n| ------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |\n| `actionId`   | `number`                         | Id задачи в планировщике.                                                                                         |\n| `url`        | `string` или `URL` или `Request` | Url запроса.                                                                                                      |\n| `requestId`  | `number`                         | Id отдельного запроса. Одна задача может выполнятся несколько раз при повторе в случае ошибки.                    |\n| `startTime`  | `number`                         | [Время начала запроса](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) |\n\n#### `response` - получение ответа\n\nСодержит все поля из события `request` и дополнительно:\n\n| Наименование   | Тип      | Описание                                                                                                                                                            |\n| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `responseType` | `string` | Тип запроса. `OK` - успешный запрос; `RATE_LIMIT_OVERFLOW` - ошибка 429 TooManyRequests; `PARALLEL_LIMIT_OVERFLOW` - ошибка превышения лимита параллельных запросов |\n| `endTime`      | `number` | Время получения ответа                                                                                                                                              |\n\n#### `fetch-error` - ошибка при попытке выполнить запрос\n\nСодержит все поля из события `request` и дополнительно:\n\n| Наименование | Тип      | Описание               |\n| ------------ | -------- | ---------------------- |\n| `error`      | `Error`  | Объект ошибки          |\n| `errorTime`  | `number` | Время получения ошибки |\n\n## Пример: Разделение лимита между разными процессами\n\nАлгоритм планировщика позволяет динамически управлять частотой запросов в зависимости от значения текущих лимитов API МойСклад. Поэтому даже при одновременной работе нескольких приложений в разных процессах нагрузка не будет превышать в среднем 15 запросов в секунду (в соответствии с лимитами API МойСклад - не более 45 запросов в течение 3-х секунд).\n\nНа графике, по собранной статистике из реального теста, видно как пять интенсивно выполняющих запросы приложения из разных процессов автоматически делят между собой доступный лимит.\n\nПо оси X время (кол-во секунд с момента запуска первого процесса), по оси Y кол-во запросов в секунду. Общая нагрузка, независимо от кол-ва работающих приложений, в любой момент времени соответствует ~15 запросам в секунду.\n\n![Разделение лимита между процессами](./resource/stress.svg)\n\nОтдельные пики при старте очередного приложения связаны с тем, что в момент запуска планировщик \"не знает\" о текущих лимитах и первые запросы выполняются без задержки.\n\nКонечно, для достижения такого эффекта нужно чтобы каждое приложение работало через планировщик запросов.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwmakeev%2Fmoysklad-fetch-planner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwmakeev%2Fmoysklad-fetch-planner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwmakeev%2Fmoysklad-fetch-planner/lists"}