TGViewer
Настоящий JavaScript Настоящий JavaScript @true_js · 5.89K subscribers
Post #3627 284
⁣Теневые утечки памяти в AsyncLocalStorage + Promise.allSettled: баг, который вы не заметите до продакшена

Вы используете AsyncLocalStorage для контекста запроса, обрабатываете пачку промисов через Promise.allSettled — и вроде всё работает. Но через несколько часов нагрузочного тестирования память растёт, GC не успевает, а контекст в некоторых тасках начинает «перемешиваться». Знакомо?

Типовой баг в production
Вот как выглядит код, который ломается под нагрузкой:
import { AsyncLocalStorage } from 'async_hooks';
const storage = new AsyncLocalStorage();

async function handler() {
storage.run({ userId: 1 }, async () => {
const results = await Promise.allSettled([
fetchUser(2),
fetchUser(3),
]);
results.forEach((r) => {
if (r.status === 'fulfilled') {
// Иногда storage.getStore() возвращает undefined или чужой userId
console.log(storage.getStore()?.userId);
}
});
});
}

AsyncLocalStorage привязывается через async_hooks. Promise.allSettled создаёт новые микрозадачи, которые могут «отцепиться» от родительского контекста, если между ними есть синхронный код. В production при 50 параллельных запросах это даёт 0.1-1% перемешивания контекста — незаметно в unit-тестах, но критично для авторизации.

Утечка памяти
При каждом storage.run() создаётся новый экземпляр хранилища. Promise.allSettled с 1000 внутренних промисов сохраняет ссылки на контекст через async_hooks — GC освобождает память только после завершения всех промисов. Если один из них «зависает» из-за необработанного reject в обработчике then/catch, утечка растёт бесконечно. В Node 14-16 это системная проблема, воспроизводимая через 1000 параллельных вызовов handler() с 10 подзапросами внутри.

Практический совет
Если не можете обновить Node.js до 20+ (где async_hooks исправлены), избегайте Promise.allSettled внутри storage.run(). Вместо этого разбейте на отдельные вызовы run для каждого запроса. Для критических мест передавайте контекст через параметры функции, а не через глобальное хранилище — это устраняет и утечки, и перемешивание.

Предупреждение
Типичная ошибка — считать, что AsyncLocalStorage гарантированно возвращает правильный контекст в любом асинхронном коде. На деле Promise.allSettled с его микрозадачами нарушает это в старых версиях Node. Не доверяйте глобальному контексту без проверки: добавляйте логирование storage.getStore() в ключевых точках под нагрузкой.

Вывод: Promise.allSettled и AsyncLocalStorage несовместимы в старых версиях Node — проверяйте свою среду и передавайте контекст явно, чтобы избежать багов, которые невозможно отловить в unit-тестах.
  • ❤ 1
More from @true_js
  1. Oct 3, 2026Post #3963
  2. Oct 2, 2026😅 Айтишник отправил в одну компанию три одинаковых резюме и только одно дошло до финала О…
  3. Oct 2, 2026Post #3961
  4. Oct 2, 2026🤣 Правильно расставленные приоритеты в моей жизни би лайк: 💥 xCode Journal
  5. Oct 1, 2026🤯 Люди взбунтовались против «пыточной для ИИ» На GitHub заметили открытый проект AI Tortu…
  6. Oct 1, 2026День в Заонежье начинается ещё в дороге. Мы встретим вас в аэропорту или на вокзале. Дальш…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →