Сценарий · Inquir Compute

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.

HTTP принимает, пайплайн обрабатывает по частям

HTTP-функция получает URL файла и сразу возвращает { jobId }, а пайплайн скачивает CSV, режет его на чанки и обрабатывает каждый как отдельный шаг с собственными ретраями.

Ключ идемпотентности — хэш строки или составной ключ; ON CONFLICT DO NOTHING гарантирует, что повторный прогон не создаст дублей.

Что нужно для надёжного CSV-импорта

Асинхронный приём и постановка в пайплайн

HTTP-функция принимает ссылку на файл и возвращает jobId, а пайплайн скачивает CSV и режет его на чанки — соединение не блокируется на время импорта.

Идемпотентная вставка или обновление (UPSERT)

Каждая строка записывается по уникальному ключу, поэтому повторный прогон того же файла не создаёт дублей.

Обработка файла по частям в шагах пайплайна

Каждый фрагмент файла — отдельный шаг пайплайна: при сбое ретраится только упавший фрагмент, а не весь импорт.

Прогресс

Обновляйте счётчик в базе после каждого чанка — клиент опрашивает статус по jobId и видит, сколько строк уже обработано.

Как организовать CSV-импорт

1

Принять файл и вернуть 202

HTTP-функция сохраняет файл или ссылку, вызывает global.durable.startNew('process-csv', undefined, { fileUrl, importId }) и возвращает { importId, status: 'pending' } с кодом 202.

2

Разбить на чанки в пайплайне

Первый шаг пайплайна читает заголовки и разбивает файл на батчи по N строк.

3

Upsert и трекинг прогресса

Каждый шаг делает UPSERT батча и обновляет счётчик прогресса; итог фиксируется после последнего шага.

Async CSV-импорт по частям

HTTP-функция сразу возвращает 202 и запускает обработчик CSV как durable-инстанс через global.durable.startNew — обычную функцию, которая переживает рестарты и адресуется по id. instanceId делает старт идемпотентным; для очень больших файлов разнесите работу на несколько инстансов по диапазонам строк.

api/import-csv.mjs (HTTP handler)
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' }) };
}
jobs/process-csv.mjs (durable function)
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 пайплайна — большой файл не попадает в память функции целиком.

Что делать при ошибке в одной строке?

Пропустите строку, записав её в лог, и продолжите чанк — или завершите чанк с ошибкой, чтобы он ушёл в ретрай. Выбор зависит от требований к целостности данных.