Node.js process.availableMemory 가이드: 메모리 압박을 안전하게 감지하는 법
Node.js 애플리케이션에서 메모리 문제는 갑자기 터지는 것처럼 보일 때가 많습니다. 대량 파일을 읽거나, 큰 JSON을 변환하거나, 캐시를 한 번에 데우는 작업은 평소에는 잘 동작하다가 특정 입력 크기에서 프로세스를 압박합니다. 특히 컨테이너 환경에서는 호스트 전체 메모리가 충분해 보여도, 컨테이너에 할당된 한도를 넘으면 프로세스가 종료될 수 있습니다.
process.availableMemory()는 현재 프로세스가 사용할 수 있는 메모리 여유를 바이트 단위로 확인할 수 있는 Node.js API입니다.
이 값만으로 모든 메모리 정책을 결정하면 위험하지만, 배치 작업의 크기 조절, 큰 요청의 사전 거절, 메모리 압박 지표 수집에는 유용한 신호가 됩니다.
이 글에서는 Node.js process.availableMemory() 기본 사용법, heapUsed와의 차이, process.constrainedMemory()와 함께 보는 법, 실무 예제와 운영 주의점을 정리합니다.
메모리 누수 분석이 목적이라면 Node.js memory leak 가이드: heapdump와 Clinic.js로 누수 찾기도 함께 보면 좋습니다.
process.availableMemory가 필요한 이유
heapUsed만 보면 런타임 전체 여유를 놓칠 수 있다
Node.js에서 메모리를 확인할 때 가장 먼저 떠올리는 값은 process.memoryUsage()입니다.
이 API는 V8 힙, 외부 메모리, RSS 같은 프로세스 사용량을 보여 줍니다.
문제는 “지금 얼마나 쓰고 있는가”와 “앞으로 얼마나 더 쓸 수 있는가”가 같은 질문이 아니라는 점입니다.
const usage = process.memoryUsage();
console.log({
rss: usage.rss,
heapUsed: usage.heapUsed,
heapTotal: usage.heapTotal,
external: usage.external,
arrayBuffers: usage.arrayBuffers,
});
heapUsed가 낮아도 네이티브 버퍼, 압축 해제 작업, 이미지 변환, 큰 Buffer 할당은 별도 영역에서 압박을 만들 수 있습니다.
반대로 힙 사용량이 일시적으로 높아도 가비지 컬렉션 뒤에 회복될 수 있습니다.
그래서 운영 판단에는 사용량과 여유량을 같이 보는 편이 안전합니다.
컨테이너 한도를 고려한 보수적 신호가 필요하다
서버가 Docker, Kubernetes, PaaS 위에서 실행된다면 실제 의사결정 기준은 호스트 메모리보다 프로세스가 접근 가능한 한도입니다.
컨테이너 한도를 넘어서는 순간 운영체제나 런타임이 프로세스를 종료할 수 있습니다.
이때 process.availableMemory()는 현재 프로세스 관점에서 남은 메모리를 확인하는 데 도움을 줍니다.
console.log(`available memory: ${process.availableMemory()} bytes`);
이 값은 “이만큼을 반드시 할당해도 된다”는 보증이 아닙니다. 동시에 다른 작업이 메모리를 사용할 수 있고, 가비지 컬렉션 타이밍도 영향을 줍니다. 하지만 큰 작업을 시작하기 전, 이미 여유가 너무 적다면 더 안전한 경로를 선택할 수 있습니다.
기본 사용법
바이트 값을 사람이 읽기 좋은 단위로 바꾼다
process.availableMemory()는 바이트 단위 숫자를 반환합니다.
로그나 대시보드에 그대로 찍으면 읽기 어려우므로 MiB 단위 변환 함수를 두는 편이 좋습니다.
function toMiB(bytes) {
return Math.round(bytes / 1024 / 1024);
}
const available = process.availableMemory();
console.log(`available memory: ${toMiB(available)} MiB`);
운영 로그에서는 단위가 섞이는 것이 장애 분석을 어렵게 만듭니다. 메모리 관련 지표는 바이트로 저장하고, 사람이 읽는 메시지에서만 MiB나 GiB로 변환하는 방식을 추천합니다.
API 지원 여부를 확인하는 래퍼를 만든다
오래된 Node.js 버전까지 지원해야 한다면 API 존재 여부를 확인해야 합니다. 런타임 버전이 섞인 서비스에서는 작은 호환 래퍼를 만들어 두면 호출부가 단순해집니다.
export function getAvailableMemoryBytes() {
if (typeof process.availableMemory === 'function') {
return process.availableMemory();
}
return null;
}
const available = getAvailableMemoryBytes();
if (available == null) {
console.warn('process.availableMemory() is not supported in this runtime');
}
null을 반환하게 만들면 호출부에서 “모름”과 “0바이트”를 구분할 수 있습니다.
메모리 정책에서 이 구분은 중요합니다.
값을 알 수 없는 상황을 0으로 취급하면 모든 작업을 거절할 수 있고, 무한대로 취급하면 보호 장치가 사라질 수 있습니다.
constrainedMemory와 함께 보기
제한된 메모리 한도를 같이 기록한다
process.constrainedMemory()는 프로세스에 적용된 메모리 제한을 바이트 단위로 알려 줍니다.
제한을 감지하지 못하면 0을 반환할 수 있습니다.
availableMemory()와 함께 기록하면 현재 여유와 런타임 한도를 같은 맥락에서 볼 수 있습니다.
function readMemorySnapshot() {
const usage = process.memoryUsage();
const constrained = process.constrainedMemory?.() ?? 0;
const available = process.availableMemory?.() ?? null;
return {
rss: usage.rss,
heapUsed: usage.heapUsed,
heapTotal: usage.heapTotal,
external: usage.external,
constrained,
available,
};
}
console.log(readMemorySnapshot());
컨테이너 한도가 있는 환경에서는 constrained가 의미 있는 기준이 됩니다.
반대로 로컬 개발 환경처럼 제한이 없거나 감지되지 않는 곳에서는 0이 나올 수 있으므로, 이 값을 그대로 나눗셈에 쓰면 안 됩니다.
런타임 진단 정보를 남기는 방식은 Node.js process report 가이드: 운영 장애 진단 정보를 남기는 법과 함께 설계하면 좋습니다.
비율은 한도가 있을 때만 계산한다
메모리 압박을 비율로 표현하려면 분모가 확실해야 합니다.
constrainedMemory()가 0이면 제한을 알 수 없다는 뜻으로 다루고, 절대 여유량 기준만 적용하는 편이 안전합니다.
export function getMemoryPressure() {
const constrained = process.constrainedMemory?.() ?? 0;
const available = process.availableMemory?.() ?? null;
if (!available) {
return { level: 'unknown', available, constrained };
}
if (constrained > 0) {
const usedRatio = 1 - available / constrained;
if (usedRatio >= 0.9) {
return { level: 'critical', available, constrained, usedRatio };
}
if (usedRatio >= 0.75) {
return { level: 'warning', available, constrained, usedRatio };
}
return { level: 'normal', available, constrained, usedRatio };
}
if (available < 128 * 1024 * 1024) {
return { level: 'warning', available, constrained };
}
return { level: 'normal', available, constrained };
}
기준값은 서비스마다 달라야 합니다. 이미지 처리처럼 한 번에 큰 버퍼를 만드는 서비스와 짧은 API 요청 위주의 서비스는 안전 여유가 다릅니다. 처음에는 보수적으로 경고만 남기고, 실제 장애 기록과 지표를 보며 임계값을 조정하는 편이 좋습니다.
큰 작업을 시작하기 전에 확인하기
최소 여유 메모리 기준을 둔다
대량 작업을 시작하기 전에는 입력 크기와 메모리 여유를 함께 확인할 수 있습니다. 예를 들어 CSV 전체를 메모리로 읽는 작업이라면 파일 크기의 몇 배가 필요할 수 있습니다. 파싱 과정에서 문자열, 객체, 배열이 추가로 만들어지기 때문입니다.
import { stat } from 'node:fs/promises';
function hasEnoughMemoryForFile(fileSize, multiplier = 4) {
const available = process.availableMemory?.();
if (available == null) {
return { ok: true, reason: 'memory availability is unknown' };
}
const estimatedNeed = fileSize * multiplier;
const safetyMargin = 64 * 1024 * 1024;
return {
ok: available > estimatedNeed + safetyMargin,
available,
estimatedNeed,
};
}
export async function assertCanImportFile(filePath) {
const file = await stat(filePath);
const decision = hasEnoughMemoryForFile(file.size);
if (!decision.ok) {
throw new Error('Not enough memory to import this file safely');
}
return decision;
}
이 예제의 배수와 안전 마진은 정답이 아닙니다. 서비스의 파싱 방식, 입력 형식, 동시 실행 개수에 맞게 조정해야 합니다. 중요한 것은 큰 작업을 무조건 시작한 뒤 실패하는 대신, 시작 전에 거절하거나 더 작은 처리 방식으로 전환하는 기준을 코드에 남기는 것입니다.
거절보다 스트리밍 전환이 더 나은 경우가 많다
메모리가 부족하다고 해서 항상 요청을 실패시킬 필요는 없습니다. 가능하다면 전체 파일을 한 번에 읽지 말고 스트림으로 나누어 처리하는 편이 더 좋은 사용자 경험을 만듭니다.
export async function chooseImportMode(filePath) {
const file = await stat(filePath);
const available = process.availableMemory?.() ?? null;
if (available != null && available < file.size * 4) {
return 'streaming';
}
if (file.size > 50 * 1024 * 1024) {
return 'streaming';
}
return 'in-memory';
}
대용량 처리는 메모리 신호와 입력 크기 정책을 함께 써야 합니다. 스트림의 backpressure까지 고려하면 프로세스가 한 번에 들고 있는 데이터 양을 줄일 수 있습니다. 기본 개념은 Node.js stream backpressure 가이드: 메모리 스파이크를 막는 법을 참고하면 좋습니다.
배치 크기를 동적으로 조절하기
여유 메모리가 적을수록 배치를 줄인다
큐 작업이나 데이터 변환 작업에서는 배치 크기를 고정하지 않고 현재 메모리 상황에 맞게 조절할 수 있습니다. 메모리 여유가 충분할 때는 처리량을 우선하고, 압박이 커지면 한 번에 처리하는 항목 수를 줄이는 방식입니다.
export function chooseBatchSize({ defaultSize = 1000, minSize = 100 } = {}) {
const available = process.availableMemory?.();
if (available == null) {
return defaultSize;
}
const mib = available / 1024 / 1024;
if (mib < 128) {
return minSize;
}
if (mib < 512) {
return Math.max(minSize, Math.floor(defaultSize / 2));
}
return defaultSize;
}
이런 정책은 작업 자체가 작은 단위로 나뉘어 있을 때 효과가 큽니다. 단일 항목 하나가 매우 큰 메모리를 요구한다면 배치 크기를 줄여도 문제가 해결되지 않습니다. 그 경우에는 입력 제한, 스트리밍 처리, 워커 분리, 외부 저장소 활용 같은 구조 변경이 필요합니다.
양보 지점과 함께 쓰면 응답성을 지킬 수 있다
배치 작업이 길게 이어진다면 메모리뿐 아니라 이벤트 루프 응답성도 신경 써야 합니다.
배치 사이에 scheduler.yield()를 넣으면 다른 타이머나 요청 처리 콜백이 실행될 기회를 얻습니다.
import { scheduler } from 'node:timers/promises';
export async function processItems(items, handleItem) {
let batchSize = chooseBatchSize();
for (let index = 0; index < items.length; index += 1) {
await handleItem(items[index]);
if ((index + 1) % batchSize === 0) {
await scheduler.yield();
batchSize = chooseBatchSize();
}
}
}
메모리 압박이 커진 시점에 배치 크기를 다시 계산하면, 작업 중간에도 정책을 조정할 수 있습니다. 이벤트 루프 양보가 필요한 이유는 Node.js scheduler.yield 가이드: 긴 작업에서 이벤트 루프를 안전하게 양보하는 법에서 더 자세히 다룹니다.
관측 지표로 남기기
요청 로그에 매번 남기기보다 주기적으로 샘플링한다
메모리 지표는 유용하지만 모든 요청마다 상세 로그로 남기면 로그 비용이 커집니다. 대부분의 서비스에서는 일정 주기로 샘플링하고, 위험 수준일 때만 경고 로그를 남기는 방식이 충분합니다.
export function startMemoryPressureLogger({ intervalMs = 30_000 } = {}) {
const timer = setInterval(() => {
const pressure = getMemoryPressure();
if (pressure.level === 'warning' || pressure.level === 'critical') {
console.warn('memory pressure detected', pressure);
return;
}
console.info('memory pressure snapshot', pressure);
}, intervalMs);
timer.unref();
return () => clearInterval(timer);
}
timer.unref()를 호출하면 이 타이머 하나 때문에 프로세스 종료가 지연되는 일을 줄일 수 있습니다.
운영 서비스에서는 로그뿐 아니라 Prometheus, OpenTelemetry, APM 같은 관측 도구에 게이지 지표로 보내는 편이 더 좋습니다.
Node.js에서 관측 신호를 분리하는 패턴은 Node.js diagnostics_channel 가이드: 관측 코드를 안전하게 분리하는 법과도 잘 맞습니다.
경고는 원인 추적에 필요한 맥락을 함께 남긴다
메모리 경고가 발생했을 때 값 하나만 있으면 원인을 찾기 어렵습니다. 가능하면 현재 작업 종류, 큐 길이, 입력 크기, 동시 실행 개수 같은 맥락을 함께 남겨야 합니다.
export function logMemoryBeforeJob(job) {
const pressure = getMemoryPressure();
if (pressure.level === 'critical') {
console.error('job blocked by critical memory pressure', {
jobType: job.type,
jobId: job.id,
inputBytes: job.inputBytes,
pressure,
});
return false;
}
console.info('job memory precheck passed', {
jobType: job.type,
jobId: job.id,
pressure,
});
return true;
}
단, 로그에 사용자 개인정보나 토큰을 넣으면 안 됩니다. 작업 식별자는 내부 추적에 필요한 최소한으로 남기고, 입력 파일명이나 요청 본문처럼 민감할 수 있는 값은 마스킹해야 합니다. 로그 예시를 안전하게 다루는 법은 로그 예제 마스킹 가이드: 신뢰할 수 있는 개발 글을 위한 정리법을 참고할 수 있습니다.
운영 시 주의할 점
availableMemory는 용량 예약이 아니다
process.availableMemory()가 충분한 값을 반환했다고 해서 다음 할당이 반드시 성공한다는 뜻은 아닙니다.
다른 요청이 동시에 들어올 수 있고, 네이티브 애드온이나 외부 라이브러리가 예상보다 많은 메모리를 쓸 수 있습니다.
따라서 이 값은 허가서가 아니라 위험 신호로 다루는 편이 맞습니다.
큰 작업에는 다음 원칙을 함께 적용하는 것이 좋습니다.
- 한 번에 읽는 데이터 크기를 제한한다.
- 스트리밍 또는 페이지 단위 처리로 전환할 수 있게 만든다.
- 동시 실행 개수를 제한한다.
- 실패 시 재시도보다 안전한 중단과 재큐잉을 우선한다.
- 메모리 경고가 반복되면 입력 크기 정책을 재검토한다.
동시 실행 제어가 필요하다면 Node.js concurrency limit 가이드: Promise 풀로 과부하를 줄이는 법을 함께 보면 좋습니다.
GC를 직접 호출하는 해결책에 기대지 않는다
메모리가 부족해 보일 때 global.gc()를 떠올릴 수 있습니다.
하지만 이 방식은 --expose-gc가 필요하고, 호출 시점에 애플리케이션 지연을 만들 수 있으며, 구조적인 메모리 증가를 해결하지 못합니다.
운영 코드의 기본 방어선은 강제 GC가 아니라 입력 제한, 배치 분리, 스트리밍, 동시성 제한이어야 합니다.
export function shouldDeferHeavyJob() {
const pressure = getMemoryPressure();
return pressure.level === 'warning' || pressure.level === 'critical';
}
작업을 잠시 미루는 정책은 강제 정리보다 예측 가능합니다. 큐 기반 시스템이라면 메모리 압박이 낮아질 때까지 재시도 지연을 두고, API 요청이라면 명확한 오류 메시지와 함께 더 작은 입력을 안내할 수 있습니다.
실무 적용 체크리스트
도입 전에 정할 것
- 지원할 Node.js 최소 버전과
process.availableMemory()호환 정책을 정한다. - 컨테이너 메모리 제한과 Node.js 힙 옵션을 함께 문서화한다.
- 큰 파일, 큰 JSON, 대량 배치 작업의 입력 크기 제한을 정한다.
- 메모리 압박 시 거절, 스트리밍 전환, 재큐잉 중 어떤 전략을 쓸지 정한다.
- 경고 로그에 남길 맥락과 마스킹 규칙을 정한다.
코드 리뷰에서 볼 것
availableMemory값을 절대 보증처럼 사용하지 않았는가?constrainedMemory()가0일 때 나눗셈 오류가 나지 않는가?- API 미지원 런타임에서 안전하게 동작하는가?
- 큰 입력을 처리하기 전에 사전 점검이나 스트리밍 경로가 있는가?
- 로그에 파일 경로, 사용자 데이터, 토큰 같은 민감정보가 그대로 남지 않는가?
FAQ
process.availableMemory와 process.memoryUsage 중 무엇을 써야 하나요?
둘 다 보는 것이 좋습니다.
process.memoryUsage()는 현재 프로세스가 얼마나 쓰는지 보여 주고, process.availableMemory()는 현재 프로세스 관점의 남은 여유를 보여 줍니다.
원인 분석에는 사용량 지표가 필요하고, 사전 방어에는 여유량 지표가 유용합니다.
availableMemory가 낮으면 바로 요청을 차단해야 하나요?
항상 그렇지는 않습니다. 작은 요청까지 차단하면 장애 범위가 커질 수 있습니다. 대용량 업로드, 배치 재계산, 캐시 워밍처럼 메모리를 크게 쓰는 작업부터 보수적으로 제한하고, 일반 요청은 지표를 보며 단계적으로 정책을 적용하는 편이 좋습니다.
컨테이너에서는 constrainedMemory가 항상 설정되나요?
환경에 따라 다를 수 있습니다.
메모리 제한을 감지하지 못하면 0을 반환할 수 있으므로, 코드에서는 0을 “제한 없음”이 아니라 “확실한 제한 값을 알 수 없음”으로 다루는 편이 안전합니다.
이 API로 메모리 누수를 찾을 수 있나요?
직접적인 누수 탐지 도구는 아닙니다. 여유 메모리가 계속 줄어드는 추세를 경고 신호로 볼 수는 있지만, 원인을 찾으려면 힙 스냅샷, 프로파일링, RSS 추적, 요청별 작업량 분석이 필요합니다.
마무리
process.availableMemory()는 Node.js 서비스가 메모리 압박을 더 일찍 감지하게 해 주는 실용적인 신호입니다.
다만 이 값은 안전한 할당을 보장하지 않으므로, 입력 제한과 스트리밍 처리, 배치 크기 조절, 동시성 제한과 함께 사용해야 합니다.
운영 코드에서는 process.memoryUsage(), process.constrainedMemory(), process.availableMemory()를 함께 기록하고, 큰 작업을 시작하기 전에는 보수적인 사전 점검을 두는 것이 좋습니다.
그렇게 하면 프로세스가 갑자기 종료된 뒤 원인을 찾는 대신, 위험한 작업을 미리 늦추거나 더 안전한 처리 경로로 전환할 수 있습니다.
이 글은 AI가 작성/정리하고, 오너가 방향을 결정한 프로젝트 운영 로그입니다.