람다 API의 p99 지연이 가끔 1초를 넘어서 프로비저닝된 동시성을 켜야 하나, SnapStart로 충분한가, 웜업 핑이면 되나 고민하는 경우가 많아요. 결론부터 말하면 먼저 로그의 Init Duration으로 콜드 스타트가 실제로 몇 번, 얼마나 길게 나는지 재요. 그다음 사용자 대면 API처럼 지연 기준이 엄격하면 프로비저닝된 동시성, 런타임이 Java·Python·.NET이고 초기화가 무거우면 SnapStart, 둘 다 아니면 초기화 코드부터 줄여요. 웜업 핑은 동시 요청이 몰릴 때 대부분 소용이 없어요.
이 글은 실무 판단을 다뤄요. 상황별 판단표, 수량 공식, 미국 동부 요금으로 직접 계산한 손익분기, 증상별 확인 순서, 도입 전 체크리스트를 정리했어요.
먼저 재 보기 — 콜드 스타트가 정말 병목인가
람다 실행 환경 생명주기 문서는 콜드 스타트가 보통 호출의 1% 미만에서 나고, 길이는 100ms 미만부터 1초 넘게까지 다양하다고 적어요. 호출이 드문 개발 환경의 첫 호출 지연만 보고 운영 설정을 바꾸면 돈만 더 쓰기 쉬워요.
콜드 스타트가 난 호출은 CloudWatch 로그의 REPORT 줄에 Init Duration 항목이 붙어요. CloudWatch Logs Insights에서 함수 로그 그룹을 골라 시간대별로 세어 봐요.
filter @type = "REPORT"
| stats count(*) as calls,
count(@initDuration) as cold,
pct(@initDuration, 50) as init_p50,
pct(@initDuration, 99) as init_p99,
pct(@duration, 99) as dur_p99
by bin(1h)
count(@initDuration)은 값이 있는 줄만 세서 콜드 스타트 수가 돼요. 이 세 숫자로 판단해요.
- cold ÷ calls 가 1%보다 훨씬 크면 트래픽이 들쭉날쭉하거나 배포가 잦다는 뜻이에요. 설정보다 원인부터 봐요.
- init_p99 가 수백 ms 이하면 초기화 코드 정리만으로 끝날 때가 많아요.
- init_p99 가 수 초이고 사용자 응답 경로 위라면 이 글의 판단표로 넘어가요.
JSON 로그 형식이면 platform.initReport 같은 이벤트가 한 줄 JSON으로 찍혀요. JSON Formatter에 붙여 펼치면 읽기 쉬워요.
상황별 판단표 — 무엇부터 손대나
| 상황 | 먼저 고를 것 | 고르지 않는 이유가 있는 것 |
|---|---|---|
| 사용자 대면 API, p99 기준이 엄격함 | 프로비저닝된 동시성 + 별칭 | 웜업 핑: 동시 요청 수만큼 데우지 못해요 |
| Java·Python 3.12+·.NET 8+, 프레임워크 로딩이 수 초 | SnapStart | 프로비저닝된 동시성과 함께 켤 수 없어요 |
| Node.js·Ruby·OS 전용 런타임, 초기화가 무거움 | 패키지·import 다이어트, 그다음 프로비저닝된 동시성 | SnapStart 미지원 런타임이에요 |
| 큐·배치 같은 비동기 처리 | 아무것도 켜지 않음 | 지연에 둔감해서 추가 요금만 늘어요 |
| 출근 시간처럼 피크가 예측됨 | 예약(scheduled) 스케일링으로 시간대만 켜기 | 24시간 상시 수량은 밤에 이용률이 떨어져요 |
| 하루 몇 번만 불리는 함수 | 그대로 두기 | SnapStart도 드문 호출에는 효과가 작다고 문서가 적어요 |
SnapStart 문서는 SnapStart로 몇 초 걸리던 초기화를 최적의 경우 1초 미만까지 줄일 수 있다고 소개해요. 동시에, SnapStart로 못 맞추는 엄격한 지연 요구에는 프로비저닝된 동시성을 쓰라고 적어요. 두 자릿수 ms 응답을 목표로 설계된 쪽이 프로비저닝된 동시성이에요.
어느 길이든 초기화 코드 다이어트는 공통이에요. 생명주기 문서도 실행 전 지연의 가장 큰 몫을 초기화 코드로 꼽아요.
쓰지 않는 라이브러리는 빼고, 한 경로에서만 쓰는 무거운 클라이언트는 처음 필요할 때 만들고, 재사용할 연결만 핸들러 바깥에 둬요.
프로비저닝된 동시성 수량과 설정값 정하기
프로비저닝된 동시성 문서가 주는 공식은 하나예요.
필요 동시성 = 평균 초당 요청 수 × 평균 실행 시간(초)
초당 50건, 평균 200ms라면 50 × 0.2 = 10이에요. 문서는 평소 피크에 10% 여유를 더하라고 권해요. 피크 동시성이 20이면 22개를 잡는 식이에요. 초당 요청 수는 Invocations, 실행 시간은 Duration 지표로 구해요.
| 설정 | 정할 값 | 이유 |
|---|---|---|
| 붙일 대상 | 버전을 가리키는 별칭 | $LATEST에는 붙지 않아요. API Gateway·이벤트 소스도 그 별칭을 가리켜야 해요 |
| 수량 상한 | 미예약 동시성 − 100 | 계정 한도 1,000이면 함수 하나에 최대 900이에요 |
| 자동 조절 방식 | 예측 가능하면 예약, 아니면 목표 추적 | 문서가 이렇게 나눠 권해요 |
| 목표 추적 목표값 | 0.7 부근(10~90% 사이) | 70%를 넘으면 늘리고, 63%(목표의 90%) 아래면 줄여요 |
| 경보 통계 | 짧은 폭주가 많으면 Maximum | 기본 Average는 3분 넘게 이어진 부하에만 반응해요 |
| 최소 용량 | 1 이상 직접 지정 | 처음 값이 없으면 자동 조절이 제대로 동작하지 않을 수 있어요 |
조심할 점이 하나 더 있어요. ProvisionedConcurrencyUtilization 지표는 함수가 요청을 받을 때만 나와요. 요청이 끊기면 경보가 INSUFFICIENT_DATA가 되고 수량이 줄지 않아요. 밤새 요청이 없는 함수라면 목표 추적만 믿지 말고 예약 스케일링으로 밤에는 내려 둬요.
숫자로 보는 비용 — 이용률 60%가 손익분기
미국 동부(버지니아 북부) x86 요금을 AWS Lambda 요금 페이지에서 확인했어요(2026년 10월 기준, 바뀔 수 있어요).
- 온디맨드 실행 시간: GB-초당 $0.0000166667
- 프로비저닝된 동시성 유지: GB-초당 $0.0000041667
- 프로비저닝된 동시성 위 실행 시간: GB-초당 $0.0000097222
- 요청: 100만 건당 $0.20 (두 방식 같음)
프로비저닝된 동시성 1개를 한 달(730시간) 켜 두고, 그중 비율 u만큼 실제로 요청을 처리한다고 해 봐요. 메모리 1GB 기준으로 계산하면 이래요.
- 온디맨드로 같은 일을 하면: u × 2,628,000초 × 0.0000166667 = u × $43.80
- 프로비저닝된 동시성: 2,628,000 × 0.0000041667 + u × 2,628,000 × 0.0000097222 = $10.95 + u × $25.55
두 식이 같아지는 u는 0.0000041667 ÷ (0.0000166667 − 0.0000097222) = 0.60이에요. 이용률이 60%를 넘으면 프로비저닝된 동시성이 오히려 싸고, 그 아래면 지연을 사는 값을 내는 셈이에요.
두 선은 이용률 60%에서 만나요. Init 요금과 넘친 트래픽 요금은 넣지 않았어요.
표로 보기
| 구분 | 온디맨드 | 프로비저닝된 동시성 |
|---|---|---|
| 20% | 8.8 | 16.1 |
| 40% | 17.5 | 21.2 |
| 60% | 26.3 | 26.3 |
| 80% | 35 | 31.4 |
| 100% | 43.8 | 36.5 |
이번에는 앞의 예시(초당 50건, 평균 200ms, 1GB)를 한 달 돌린다고 해 봐요. 실행량은 50 × 0.2 × 2,628,000 = 2,628만 GB-초이고 요청 요금은 어느 쪽이든 $26.28이에요.
22개를 24시간 켜면 이용률이 10 ÷ 22 = 45%라 온디맨드보다 $58.40 비싸요. 같은 22개를 낮 12시간만 켜면 유지비가 절반($120.45)으로 줄어요. 밤에 꺼 둔 동안의 실행은 온디맨드 단가라 실제 값은 조금 더 나와요. 정리하면 "피크 기준 상시 수량"이 가장 흔하게 돈이 새는 설정이에요.
콜드 스타트 요금도 따져 봐요. 콜드 스타트가 호출의 1%(월 131만 4,000번)이고 Init이 0.8초라면 131만 4,000 × 0.8 GB-초 × 0.0000166667 = 약 $17.52예요. 설정 실수 한 번이 이보다 훨씬 큰 돈이에요.
SnapStart는 Java 관리형 런타임이면 추가 요금이 없어요. Python·.NET은 두 가지가 붙어요.
- 스냅숏 캐시: GB-초당 $0.0000015046. 1GB 버전 하나를 한 달 두면 2,628,000 × 0.0000015046 = 약 $3.95이고, 최소 3시간이 과금돼요.
- 복원: 메모리 GB당 $0.0001397998. 1GB 함수가 한 달에 1만 번 복원되면 약 $1.40이에요.
캐시 요금은 SnapStart를 켠 게시 버전마다 붙어요. 옛 버전을 지우지 않으면 쌓여요.
확장 속도와 한도 — 설정보다 먼저 막히는 곳
람다 확장 동작 문서에 따르면 함수 하나는 리전마다 10초에 실행 환경 1,000개까지 늘어나요. 쓰지 않은 몫은 쌓이지 않아요. 이 속도보다 요청이 빨리 늘거나 계정 동시성 한도에 닿으면 429 스로틀 오류가 나요.
예를 들어 평소 동시성 200인 함수에 이벤트로 동시성 5,000이 필요한 트래픽이 갑자기 오면, 계정 한도가 넉넉해도 늘어나는 데 대략 40~50초가 걸려요. 그동안 새 환경은 전부 콜드 스타트이고 일부는 429를 받아요. 일정이 보이면 그 전에 예약 스케일링으로 올려 둬요.
한도 계산도 함께 해요. 계정 한도가 1,000이고 함수 A에 프로비저닝된 동시성 100을 잡으면, 다른 함수들은 나머지 900을 나눠 써요. A가 100을 다 안 써도 마찬가지라, 크게 잡으면 다른 함수가 스로틀될 수 있어요.
운영에서 자주 나는 증상과 확인 순서
프로비저닝된 동시성을 켰는데 Init Duration이 계속 찍혀요
- 호출하는 쪽이 별칭이나 버전 ARN을 쓰는지 봐요.
$LATEST나 한정자 없는 이름이면 데운 환경을 거치지 않아요. - 별칭이 가리키는 버전과 프로비저닝 설정이 붙은 버전이 같은지 봐요. 새 버전을 배포하고 별칭만 옮겼다면 설정을 다시 확인해요.
ProvisionedConcurrencySpilloverInvocations지표로 넘친 호출이 있는지 봐요. 있으면 수량이 피크보다 적은 거예요.- 함수 안에서 환경 변수
AWS_LAMBDA_INITIALIZATION_TYPE(provisioned-concurrency또는on-demand)을 로그로 남겨 확정해요.
갑자기 429가 나요
계정 ConcurrentExecutions가 한도에 닿았는지, 다른 함수의 예약·프로비저닝 수량이 풀을 잠식했는지, 10초당 1,000개 속도에 걸렸는지 봐요. 짧은 폭주라면 마지막 경우가 많아요.
REPORT의 Duration이 START~END 시간 차이보다 길어요
이전 호출이 오류나 타임아웃으로 끝나 환경이 재설정되면, 다음 호출에서 Init을 다시 도는 'suppressed init'이 생겨요. 이때는 Init 줄이 따로 없고 Duration에 Init 시간이 섞여요. 직전 호출의 오류부터 찾아요. 응답 지연 일반은 API 응답 디버깅 순서도 참고해요.
온디맨드 Init이 10초를 넘겨 첫 호출이 느려요
온디맨드 Init 제한은 10초이고, 넘기면 첫 호출 때 함수 타임아웃으로 다시 시도해요. 프로비저닝된 동시성·SnapStart는 130초와 함수 타임아웃(최대 900초) 중 큰 값까지 허용돼요.
VPC에 붙인 함수의 첫 호출이 실패해요
VPC 연결 문서에 따르면 14일 동안 쉬면 Hyperplane ENI가 회수되고 함수가 Inactive가 돼요. 다음 호출은 실패하고 ENI가 다시 준비될 때까지 Pending이에요. 월말에만 도는 함수라면 호출 쪽에 재시도를 둬요. 호출마다 ENI를 만드는 구조는 아니에요.
SnapStart를 켠 뒤 ID·토큰이 겹쳐요
Init에서 만든 UUID, 난수 시드, 임시 자격 증명은 같은 스냅숏에서 복원된 환경끼리 같아져요. 고유값은 핸들러 안이나 복원 뒤(after-restore) 런타임 훅에서 만들어요. UUID 충돌 확률 계산이 "정말 무작위일 때"를 전제한다는 점은 UUID v4 v7 구조와 충돌 확률에서 다뤘어요. 스냅숏 복제는 그 전제를 깨요.
도입 전에 따질 것 — 체크리스트
- 런타임: SnapStart는 Java 11 이상, Python 3.12 이상, .NET 8 이상만 돼요. Node.js 같은 다른 관리형 런타임과 OS 전용 런타임은 안 돼요.
- 같이 못 쓰는 기능: SnapStart는 프로비저닝된 동시성, EFS, 512MB를 넘는 임시 스토리지(/tmp)와 함께 쓸 수 없어요. /tmp를 크게 쓰는 함수라면 SnapStart는 후보에서 빠져요.
- 배포 방식: 둘 다 게시 버전과 별칭이 필요해요. 지금
$LATEST만 배포한다면 배포 파이프라인부터 바꿔야 해요. - 네트워크 연결: SnapStart 복원 뒤에는 Init 때 연 연결 상태가 보장되지 않아요. AWS SDK 연결은 대부분 자동으로 다시 붙지만, DB 드라이버 연결은 복원 뒤 확인 코드가 필요해요.
- 비용 상한: 프로비저닝된 동시성은 요청이 없어도 유지비가 나와요. 목표 추적을 쓰면 최대 용량을 꼭 정해요.
자주 묻는 질문
웜업 핑을 5분마다 보내면 프로비저닝된 동시성이 필요 없나요?
핑 한 번이 데우는 실행 환경은 하나예요. 동시 요청이 10개 몰리면 나머지 9개는 새 환경을 만들어요. 실행 환경도 계속 호출되는 함수조차 몇 시간마다 교체돼요. 예측 가능한 지연은 프로비저닝된 동시성으로 사는 편이 맞아요.
프로비저닝된 동시성은 몇 개로 시작하나요?
Invocations와 Duration 지표로 "초당 요청 수 × 평균 실행 초"를 구하고, 평소 피크 동시성에 10%를 더해요. 비용이 걱정되면 평균 동시성으로 시작해 넘친 호출 지표를 보며 올려요. 이용률이 60% 아래로 오래 머물면 수량을 줄이거나 시간대를 나눠요.
SnapStart와 프로비저닝된 동시성을 같이 켤 수 있나요?
안 돼요. SnapStart 문서에 지원하지 않는 기능으로 프로비저닝된 동시성이 적혀 있어요. 두 자릿수 ms가 필요한 경로는 프로비저닝된 동시성, 수 초짜리 초기화를 1초 안팎으로 줄이면 되는 경로는 SnapStart가 맞아요.
콜드 스타트 요금은 얼마나 나오나요?
2025년 8월부터 모든 구성에서 Init 시간이 Billed Duration에 들어가요. 1GB 함수의 Init이 0.8초면 콜드 스타트 한 번에 0.8 GB-초, 미국 동부 x86 단가로 약 $0.0000133이에요. 월 100만 번 나도 약 $13이라, 대부분은 요금보다 지연이 더 큰 문제예요.
정리
- 고치기 전에 재요.
REPORT의Init Duration으로 콜드 비율과 p99 Init 시간을 시간대별로 봐요. - 엄격한 지연 기준이면 프로비저닝된 동시성, Java·Python 3.12+·.NET 8+의 무거운 초기화면 SnapStart, 나머지는 초기화 코드 다이어트예요.
- 프로비저닝된 동시성은 이용률 60%가 손익분기예요. 피크 기준 상시 수량보다 예약 스케일링이 대개 싸요.
- 확장 속도(10초에 1,000개)와 계정 한도는 어떤 설정보다 먼저 막혀요. 큰 행사 전에는 미리 올려 둬요.
- SnapStart를 켜면 Init에서 만든 고유값을 핸들러나 복원 훅으로 옮기고, 옛 버전을 지워 캐시 요금을 막아요.