Serverless async-задачи: четыре способа поставить работу в очередь
Асинхронная работа попадает в систему через четыре точки входа: HTTP-запрос с ответом 202, входящий вебхук от провайдера, cron-расписание или шаг пайплайна, запускающий другой пайплайн. У каждой точки — свой паттерн. Эта страница собирает все четыре, чтобы вы выбрали нужный и не изобретали одни и те же handoff для каждого типа триггера.
Обновлено: 2026-06-28
Кратко
Суть ответа
Serverless async-задачи: четыре способа поставить работу в очередь. В Inquir все четыре точки входа сходятся к одному примитиву: вызов пайплайна с изоляцией «один контейнер на функцию», политикой ретраев на шаг и записью в истории выполнения (трассировки хранятся 30 дней). Тип триггера — метаданные; код хендлера остаётся одинаковым.
Когда подходит и когда нет
- HTTP 202: инициированная пользователем работа, которая не должна блокировать ответ
- Вебхук: события от провайдеров (Stripe, GitHub, Slack), где повторы провайдера требуют идемпотентности
- Cron: периодическая фоновая работа по расписанию (минимальный интервал — 5 минут)
На что обратить внимание
- Одна очередь и один воркер для всех четырёх точек входа означает, что семантика повторов для cron-задачи совпадает с вебхуком Stripe — хотя у них совершенно разные требования к идемпотентности.
- Воркер, читающий общую очередь, не различает «это пришло из вебхука, который повторяется 72 часа» и «это пришло из HTTP и клиент ждёт». Разные триггеры требуют разной обработки, а не одного цикла drain.
Ситуация: нагрузка и где обычно ломается
Четыре точки входа — четыре паттерна, которые нужно освоить
Большинство гайдов по async показывают один триггер — обычно HTTP-хендлер с вызовом очереди. Но в реальных приложениях работа приходит из четырёх направлений: от пользователя (HTTP), от сторонних провайдеров (вебхуки), по расписанию (cron) и из внутренних шагов (цепочка задач). У каждого — свои гарантии, задержки и режимы сбоя.
Ошибиться с паттерном точки входа дорого. HTTP-хендлер, ждущий результата медленной работы, завершится по таймауту. Вебхук-хендлер, делающий тяжёлую работу до ответа 200, спровоцирует повторы провайдера и дублирование эффектов. Cron без идемпотентности задвоит обработку при рестарте планировщика.
Компромиссы
Почему универсальный async-паттерн не работает
Одна очередь и один воркер для всех четырёх точек входа означает, что семантика повторов для cron-задачи совпадает с вебхуком Stripe — хотя у них совершенно разные требования к идемпотентности.
Воркер, читающий общую очередь, не различает «это пришло из вебхука, который повторяется 72 часа» и «это пришло из HTTP и клиент ждёт». Разные триггеры требуют разной обработки, а не одного цикла drain.
Как Inquir помогает в этом сценарии
Одна платформа, четыре типа триггеров, единое выполнение
В Inquir все четыре точки входа сходятся к одному примитиву: вызов пайплайна с изоляцией «один контейнер на функцию», политикой ретраев на шаг и записью в истории выполнения (трассировки хранятся 30 дней). Тип триггера — метаданные; код хендлера остаётся одинаковым.
У каждого триггера — правильный контракт: HTTP возвращает 202 немедленно; вебхук ack-ает до таймаута провайдера; cron валидирует выражение при сохранении (минимум 5 минут); цепочка передаёт выход родительского шага на вход дочернему. Async-вызовы ограничены до 120/мин на тенант.
Что вы получаете на платформе
Четыре типа async-триггеров
HTTP 202 handoff
Валидируйте вход, вызовите global.durable.startNew(), верните 202 с job reference. Вызывающий получает ответ немедленно; пайплайн работает вне HTTP-окна.
Входящий вебхук
Проверьте подпись провайдера по сырому телу, запишите ключ идемпотентности, быстро верните 200, затем поставьте тяжёлую работу в очередь. Повторы провайдера упрутся в idempotency-check.
Cron-триггер
Узел cronTrigger запускает пайплайн по cron-выражению (минимальный интервал — 5 минут). Выражения валидируются при сохранении. Каждый прогон попадает в историю выполнения.
Цепочка задача→задача
Шаг пайплайна вызывает global.durable.startNew() для запуска дочернего пайплайна — fan-out, последовательные цепочки или условные ветки по выходу родительского шага.
Что сделать дальше, по шагам
Как выбрать нужный async-триггер
Определить точку входа
Работа инициирована пользователем (HTTP), провайдером (вебхук), временем (cron) или внутренним шагом (цепочка)? Каждая — свой контракт триггера.
Применить подходящий паттерн
HTTP: 202 + startNew. Вебхук: verify + ack + startNew. Cron: cronTrigger + идемпотентный хендлер. Цепочка: startNew из шага с выходом родителя как payload.
Наблюдать все четыре в одной истории
История выполнения показывает каждый прогон независимо от типа триггера. Фильтр по функции, типу триггера или статусу без переключения дашбордов.
Пример кода
Все четыре паттерна точек входа
Каждый сниппет — минимальный паттерн точки входа. Код самого хендлера (обработка event.payload) одинаков для всех четырёх — меняется только вызов enqueue и его контекст.
export async function handler(event) { const { reportId } = JSON.parse(event.body || '{}'); if (!reportId) return { statusCode: 400, body: JSON.stringify({ error: 'reportId required' }) }; // Return immediately — job runs outside HTTP window const { instanceId: jobId } = await global.durable.startNew('generate-report', undefined, { reportId }); return { statusCode: 202, body: JSON.stringify({ jobId }) }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sig = event.headers['x-webhook-signature'] ?? ''; const expected = createHmac('sha256', process.env.WEBHOOK_SECRET).update(body).digest('hex'); if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return { statusCode: 401, body: 'invalid signature' }; const payload = JSON.parse(body); const isNew = await db.upsertEvent(payload.id, payload.type); // idempotency key if (!isNew) return { statusCode: 200, body: 'duplicate' }; await global.durable.startNew('process-event', undefined, { eventId: payload.id, type: payload.type }); return { statusCode: 200, body: 'accepted' }; }
export async function handler(event) { // event.trigger.type === 'schedule' when fired by a cronTrigger node // Cron minimum interval: 1 minute; fires within ~30s of scheduled time const since = process.env.LAST_CURSOR ?? new Date(Date.now() - 86_400_000).toISOString(); const records = await source.fetchUpdatedSince(since); if (records.length === 0) return { synced: 0 }; await destination.upsertBatch(records); // idempotent by record ID return { synced: records.length, cursor: records.at(-1)?.updatedAt }; }
export async function handler(event) { // Parent step output available as event.previousOutput const { batchId, items } = event.payload ?? {}; // Fan-out: spawn one child pipeline per item const children = await Promise.all( items.map((item) => global.durable.startNew('process-item', undefined, { batchId, itemId: item.id }) ) ); return { batchId, spawned: children.length }; }
Когда подходит и когда нет
Выберите триггер по типу точки входа
Когда это уместно
- HTTP 202: инициированная пользователем работа, которая не должна блокировать ответ
- Вебхук: события от провайдеров (Stripe, GitHub, Slack), где повторы провайдера требуют идемпотентности
- Cron: периодическая фоновая работа по расписанию (минимальный интервал — 5 минут)
- Цепочка: fan-out, последовательные шаги или условные ветки из родительского шага пайплайна
Когда лучше выбрать другое
- Работа, которая должна вернуть синхронный результат в пределах HTTP-таймаута — оставьте в хендлере
Вопросы и ответы
Вопросы и ответы
Один пайплайн может запускаться из нескольких точек входа?
Да. Один и тот же хендлер пайплайна можно вызвать из HTTP 202, вебхука или cron. Используйте event.trigger для различения контекста в хендлере при необходимости.
Есть ли встроенная очередь задач?
Вызовы пайплайнов ставятся в очередь внутренне до 100 на слот параллелизма функции. Это не durable message-очередь с гарантированным порядком — это управляемый слой выполнения. Для строгого FIFO на миллионах задач в секунду используйте отдельный message broker.
Каков минимальный интервал cron?
Пять минут. Планировщик опрашивает каждые 30 секунд, поэтому реальное время срабатывания может отставать на 30 секунд от запланированного. Суб-минутное расписание не поддерживается.
Как передать данные из запускающего шага в дочернюю задачу?
Передайте payload третьим аргументом в global.durable.startNew(). Дочерний пайплайн получит его как event.payload. Выходы родительского шага доступны как event.previousOutput в рамках того же пайплайна.