Serverless-обработка CSV: импорт больших файлов по частям
Принимайте CSV по HTTP, сразу возвращайте 202 и обрабатывайте файл в фоновом пайплайне: парсите сотни тысяч строк, валидируйте и трансформируйте данные, делайте батчевый UPSERT с ключами идемпотентности и уведомляйте пользователя о завершении импорта — всё за пределами окна HTTP-таймаута.
Обновлено: 2026-06-28
Кратко
Суть ответа
Serverless-обработка CSV: импорт больших файлов по частям. HTTP-функция получает URL файла и сразу возвращает { jobId }, а пайплайн скачивает CSV, режет его на чанки и обрабатывает каждый как отдельный шаг с собственными ретраями.
Когда подходит и когда нет
- Импорт клиентских данных, загрузка товарных каталогов и массовые миграции пользователей
- Регулярные CSV-выгрузки из внешних систем, которые нужно обрабатывать по расписанию или по триггеру
На что обратить внимание
- Стриминг файла прямо в HTTP-ответе держит соединение открытым: обрыв сети означает потерю прогресса, и продолжить с места обрыва уже не получится.
- Без обработки по частям весь файл оказывается в памяти — на больших файлах это заканчивается OOM.
Ситуация: нагрузка и где обычно ломается
Почему CSV-импорт ломает синхронные HTTP-хендлеры
- Файлы на 100 000+ строк парсятся и записываются секунды или минуты — далеко за пределами таймаута шлюза
- Синхронная обработка держит соединение открытым: клиент отваливается по таймауту или ретраит запрос и создаёт дублирующий импорт
- Загрузка всего файла в память одним проходом приводит к OOM на небольших рантаймах
Парсинг и запись 100 000 строк занимают от секунд до минут; таймаут шлюза обрывает работу на середине, и в базе остаются частично записанные данные.
Когда простых рецептов недостаточно
Почему стриминг CSV в HTTP-хендлере хрупок
Стриминг файла прямо в HTTP-ответе держит соединение открытым: обрыв сети означает потерю прогресса, и продолжить с места обрыва уже не получится.
Без обработки по частям весь файл оказывается в памяти — на больших файлах это заканчивается OOM.
Как Inquir помогает в этом сценарии
HTTP принимает, пайплайн обрабатывает по частям
HTTP-функция получает URL файла и сразу возвращает { jobId }, а пайплайн скачивает CSV, режет его на чанки и обрабатывает каждый как отдельный шаг с собственными ретраями.
Ключ идемпотентности — хэш строки или составной ключ; ON CONFLICT DO NOTHING гарантирует, что повторный прогон не создаст дублей.
Что вы получаете на платформе
Что нужно для надёжного CSV-импорта
Асинхронный приём и постановка в пайплайн
HTTP-функция принимает ссылку на файл и возвращает jobId, а пайплайн скачивает CSV и режет его на чанки — соединение не блокируется на время импорта.
Идемпотентная вставка или обновление (UPSERT)
Каждая строка записывается по уникальному ключу, поэтому повторный прогон того же файла не создаёт дублей.
Обработка файла по частям в шагах пайплайна
Каждый фрагмент файла — отдельный шаг пайплайна: при сбое ретраится только упавший фрагмент, а не весь импорт.
Прогресс
Обновляйте счётчик в базе после каждого чанка — клиент опрашивает статус по jobId и видит, сколько строк уже обработано.
Что сделать дальше, по шагам
Как организовать CSV-импорт
Принять файл и вернуть 202
HTTP-функция сохраняет файл или ссылку, вызывает global.durable.startNew('process-csv', undefined, { fileUrl, importId }) и возвращает { importId, status: 'pending' } с кодом 202.
Разбить на чанки в пайплайне
Первый шаг пайплайна читает заголовки и разбивает файл на батчи по N строк.
Upsert и трекинг прогресса
Каждый шаг делает UPSERT батча и обновляет счётчик прогресса; итог фиксируется после последнего шага.
Пример кода
Async CSV-импорт по частям
HTTP-функция сразу возвращает 202 и запускает обработчик CSV как durable-инстанс через global.durable.startNew — обычную функцию, которая переживает рестарты и адресуется по id. instanceId делает старт идемпотентным; для очень больших файлов разнесите работу на несколько инстансов по диапазонам строк.
export async function handler(event) { const { fileUrl, importId } = JSON.parse(event.body || '{}'); if (!fileUrl || !importId) return { statusCode: 400, body: JSON.stringify({ error: 'fileUrl and importId required' }) }; await db.imports.create({ id: importId, status: 'pending', fileUrl }); await global.durable.startNew('process-csv', undefined, { fileUrl, importId }); return { statusCode: 202, body: JSON.stringify({ importId, status: 'pending' }) }; }
import { parse } from 'csv-parse/sync'; export async function handler(event) { const { fileUrl, importId } = event; // the input passed to startNew() const csvText = await fetch(fileUrl).then((r) => r.text()); const rows = parse(csvText, { columns: true, skip_empty_lines: true }); let inserted = 0; for (const batch of chunk(rows, 500)) { // Upsert by external_id — idempotent on retry await db.records.upsertBatch(batch.map((r) => ({ ...r, importId }))); inserted += batch.length; } await db.imports.update(importId, { status: 'done', rowCount: rows.length }); return { importId, rows: rows.length, inserted }; }
Когда подходит и когда нет
Когда нужен async CSV-импорт
Когда это уместно
- Импорт клиентских данных, загрузка товарных каталогов и массовые миграции пользователей
- Регулярные CSV-выгрузки из внешних систем, которые нужно обрабатывать по расписанию или по триггеру
Когда лучше выбрать другое
- Небольшие CSV до 1000 строк, которые обрабатываются быстрее пяти секунд, — оставьте их синхронными, отлаживать проще
Вопросы и ответы
Вопросы и ответы
Как передать файл в пайплайн?
Загрузите его в объектное хранилище (S3/GCS) и передайте URL в payload пайплайна — большой файл не попадает в память функции целиком.
Что делать при ошибке в одной строке?
Пропустите строку, записав её в лог, и продолжите чанк — или завершите чанк с ошибкой, чтобы он ушёл в ретрай. Выбор зависит от требований к целостности данных.