새 DynamoDB 테이블은 초당 몇 건의 쓰기에서 스로틀링이 시작될까?
AWS는 갓 만든 테이블이 기본 상태에서 "up to 4,000 write request units per second"를 처리한다고 문서화합니다. 실제로 무슨 일이 일어나는지에 대해 모두가 여전히 인용하는 연구는 — Capital One과 ScyllaDB가 나란히 링크하는 그 연구는 — 2019년에 측정된 것으로, 웜 처리량도, 설정 가능한 최댓값도, 지금의 스케일링 규칙도 존재하기 전이었습니다. 저희가 아는 한 그 이후로 측정 결과를 발표한 사람은 아무도 없습니다.
그래서 저희가 직접 돌렸습니다. 2026-08-27, us-east-1에서 몇 분 전에 생성한 테이블을 상대로, 초당 1,000건에서 8,000건까지 올려 가며 쓰기 부하를 투입했습니다:
| 투입 부하 | 달성 처리량 | 스로틀링된 요청 |
|---|---|---|
| 1,000/s | 1,000/s | 0 |
| 2,000/s | 2,000/s | 0 |
| 3,000/s | 3,000/s | 0 |
| 4,000/s | 4,000/s | 0 |
| 5,000/s | 4,132/s | 25,992 |
| 6,000/s | 4,131/s | 55,966 |
| 8,000/s | 4,134/s | 115,922 |
문서에 적힌 기준선은 유효하고, 오히려 살짝 보수적입니다. 서비스는 4,000/s까지는 단 한 건의 거부도 없이 전부 수락했고, 그 위로는 아무리 세게 밀어붙여도 초당 4,130 ±2건의 쓰기에 고정되었습니다. 세 개의 구간, 세 가지 투입 부하, 오차 0.05% 이내의 동일한 상한. 읽기는 아예 한 번도 스로틀링되지 않았습니다 — 데이터를 채워 둔 테이블을 초당 12,700건이 넘는 읽기까지 몰아붙였고, 그 위에서 모자란 부분은 DynamoDB가 아니라 저희 클라이언트 쪽이었습니다.
테이블 하나, 하루, 리전 하나, 균일 난수 키를 가진 약 1 KB 항목 — 은 어디에도 없었습니다. 그 범위가 여기 나오는 모든 수치에 붙는 정직한 단서입니다. 이 글의 나머지는 그것을 어떻게 측정했는지에 대한 이야기이며, 벤치마크가 성공하기 전에 세 번 실패한 대목도 포함합니다. 그리고 그 실패 중 어느 것도 DynamoDB의 잘못이 아니었습니다.
스로틀은 400이고, 엉뚱한 용의자를 먼저 지목합니다
상한에 부딪혔을 때 돌아오는 오류는 꼼꼼히 읽어 볼 가치가 있습니다:
ThrottlingException: Throughput exceeds the current capacity of your table or index.
DynamoDB is automatically scaling your table or index so please try again shortly.
If exceptions persist, check if you have a hot key:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html모두 측정으로 확인한 관찰 세 가지:
- 5xx가 아니라 HTTP 400입니다. 재시도 정책과 대시보드가 이 사실을 알아야 합니다. 5xx만 재시도하는 클라이언트는 이 오류를 그냥 흘려버리고, 5xx에만 알람을 거는 모니터는 쓰기의 3분의 1이 튕겨 나가는 동안에도 서비스가 정상이라고 표시합니다.
- 첫 스로틀은 기준선을 넘긴 각 구간이 시작되고 0.9~3.8초 뒤에 도착했습니다 — 서비스는 상한이 걸리기 전에 짧은 유예 버스트를 주며, 투입 부하가 높을수록 더 빨리 물립니다.
- 핫 키 힌트는 진단이 아니라 기본 문구입니다. 저희 키는 균일 난수 UUID였고 핫 키는 없었습니다. 소규모에서 이 메시지의 첫 번째 용의자는 그저 테이블 수준의 상한입니다.
지연 시간은 이 모든 것에 무심했습니다. p50 쓰기 지연 시간은 스로틀링 여부와 무관하게 모든 구간에서 리전 내 4~5 ms였습니다. 서비스 입장에서 거부는 값싼 일입니다 — 느려지는 게 아니라 그냥 아니라고 말할 뿐입니다.
노트북으로는 이것을 측정할 수 없습니다
저희의 첫 계측 장치는 뻔한 것이었습니다. 마드리드의 노트북에서 돌아가는 Node 스크립트였죠. 초당 1,000건의 쓰기는 깔끔하게 맞춰 냈지만 2,000건에서 무너졌습니다 — DynamoDB가 밀어낸 것이 아니라, 대서양 왕복에 약 100 ms가 걸리는 탓에 초당 2,000건을 동시에 띄우려면 수백 개의 동시 소켓이 필요했고, 이벤트 루프가 익사했기 때문입니다. 서비스는 단 한 번도 스로틀링하지 않았습니다. 저희는 저희 Wi-Fi를 벤치마크하고 있었습니다.
두 번째 시도는 러너를 대상과 같은 리전의 Lambda 함수 하나로 옮겼습니다. 리전 내 왕복은 약 5 ms이고, 3 GB 함수 하나가 초당 2,000건의 요청을 p50 10 ms로 깔끔하게 맞춰 냈습니다. 그 위로는 CPU가 한계에 붙은 채 1,100/s 언저리에서 평평해졌습니다. 요청 서명과 응답 처리는 단일 스레드 JavaScript이고, 러너 하나로는 초당 4,000건에 서명하는 일이 애초에 불가능합니다. 할당한 메모리는 3 GB, 실제로 쓴 메모리는 253 MB였습니다. 병목은 결코 RAM이 아니었습니다 — Lambda의 CPU 몫은 메모리 설정에 따라 늘어나므로, 저희는 스토리지가 아니라 연산을 사고 있었던 셈입니다.
그래서 최종 계측 장치는 Lambda 8대로 이루어진 함대입니다. 각각이 전체 투입 부하의 8분의 1을 맞춰 내고, 모두 공유된 벽시계 T0에 맞춰 출발해 구간이 서로 정렬됩니다. 러너 8대가 각각 여유 있는 1,000/s를 담당하니 여유를 남긴 채 8,000/s의 부하를 투입할 수 있었고, 총계는 실제로 센 요청의 합입니다 — 어디에도 외삽은 없습니다.
세 번의 실행이 죽고 나서야 하나가 성공했고, DynamoDB는 매번 무죄였습니다
함대의 첫 실행은 어느 러너가 T0보다 882초 늦게 시작했다고 보고하며 끝났습니다 — 20초짜리 약속에 15분을 지각한 셈입니다. 두 번째 실행은 읽기 타임아웃으로 죽었습니다. 재시도를 끈 세 번째 실행은 러너 8대 전부에서 한꺼번에 요란하게 실패했습니다. 그동안 CloudWatch는 모든 Lambda가 4분짜리 측정을 제시간에, 오류 없이, 깔끔하게 마쳤다고 보여 주고 있었습니다.
범인은 노트북과 Lambda 사이의 연결이었습니다. 동기 호출은 실행 내내 HTTPS 연결 하나를 완전히 침묵한 채로 열어 둡니다 — 그리고 가정용 공유기는 몇 분이 지나면 조용한 연결을 조용히 끊어 버립니다. 죽은 소켓을 본 CLI는 최악의 선택을 했습니다. 조용히 재시도해서 측정 Lambda를 다시 실행했고, 그 Lambda는 자신의 T0가 한참 지나 버린 것을 발견했습니다. 모르는 사이에 두 번 실행될 수 있는 벤치마크 하니스는 하니스가 아닙니다. AWS 청구서가 딸린 난수 생성기죠.
마침내 성공한 형태에는 세 가지 규칙이 있고, 이제 저희는 오래 걸리는 원격 측정이라면 무엇에든 이 규칙을 쓸 것입니다:
- 던지고 잊기, 결과는 대역 외로. 러너는 비동기로 호출되고(연결은 밀리초 안에 닫힙니다) 결과를 작은 DynamoDB 테이블에 항목으로 씁니다. 드라이버는 결과 행 8개를 폴링합니다. 어떤 연결도 요청 하나보다 오래 살지 않습니다.
- 재시도는 어디에서나 꺼 둡니다. 측정 클라이언트는 요청당 시도 한 번으로 동작합니다 — 재시도는 저희가 세려고 존재하는 바로 그 스로틀을 조용히 흡수해 버리니까요 — 호출 경로에서도 재시도를 껐기 때문에 어떤 러너도 두 번 실행될 수 없습니다.
- 멈춤 대신 워치독. 각 러너는 자신의 일정을 마감 시각과 경주시킵니다. 무언가 끼면 조용히 타임아웃되는 대신 부분 집계와 정확히 어디에서 막혔는지 알려 주는 스냅샷을 반환합니다. 스스로를 설명하는 실패한 실행은 읽기 한 번의 비용이지만, 멈춰 버린 실행은 저녁 한나절의 비용입니다.
모든 요청에는 8초 타임아웃도 붙어 있습니다. 멈춰 버렸던 실행이 그렇게 된 이유는 타임아웃 없는 진행 중 요청 단 하나가 마지막 드레인 단계를 영원히 끼워 버렸기 때문입니다. 약 400만 건의 요청 중 경계 없는 대기 하나면 충분했습니다.
읽기는 어땠나
읽기 단계는 항목 1,000개를 채운 두 번째 새 테이블을 상대로 GetItem(각 약 1 KB, 0.5 읽기 단위)을 사용해 실행했습니다:
| 투입 부하 | 달성 처리량 | 스로틀링 |
|---|---|---|
| 4,000/s | 4,000/s | 0 |
| 8,000/s | 7,941/s | 0 |
| 12,000/s | 11,119/s | 0 |
| 16,000/s | 12,762/s | 0 |
스로틀은 단 한 건도 없었습니다. 문서에 적힌 초당 12,000 읽기 기준선은 유효하며 저희는 그 한계를 찾지 못했습니다. 16,000/s를 투입하자 러너 8대 중 5대가 자체 클라이언트 쪽 포화에 도달했으므로, 12,762/s라는 수치는 DynamoDB가 아니라 저희 함대가 도달한 천장입니다. 이것을 서비스 한계인 양 꾸미는 대신 있는 그대로 말합니다. 리전 내 읽기는 p50 2~4 ms로 동작했습니다.
기억해 둘 만한 작은 수치 두 개. 갓 만든 온디맨드 테이블이 CreateTable에서 ACTIVE가 되기까지 벤치마크 실행에서는 22초, 그 전 탐색에서는 7.4초가 걸렸습니다 — 최선의 경우가 아니라 그 편차를 기준으로 예산을 잡으세요. 그리고 벤치마크 전체, 즉 과금된 쓰기 672,116건과 읽기 108만 건의 비용은 $0.97이었습니다. 계측 장치는 재사용할 수 있고, 실험 비용은 커피 한 잔입니다.
30분간 압력을 가해도 상한이 두 배가 되지는 않습니다
AWS의 증가 규칙은 온디맨드 용량이 이전 피크의 최대 두 배까지 수용한다고 말합니다. 그것이 실제로 일어나는 것을 보고 싶어서, 상한 실행이 끝난 뒤 테이블에 초당 8,000건의 쓰기 부하를 34분 연속으로 유지하면서 달성 처리량을 10초 단위로 묶었습니다. 그 모양은 이렇습니다:
| 부하 지속 시간(분) | 상한 |
|---|---|
| 0~8 | ~4,000/s(기준선, 변동 없음) |
| 9~25 | ~5,000/s |
| 26 | ~6,000/s |
| 27~34 | ~7,000/s |
증가는 완만한 경사가 아니라 약 1,000/s씩 뚝뚝 끊기는 계단으로 옵니다 — 1분은 한 처리량에서 평평하고, 다음 1분은 그다음 처리량에서 평평합니다. 첫 상한은 연속된 초과 수요 8분을 꽉 채우는 동안 끈질기게 버팁니다. 그리고 34분이 지난 뒤 테이블은 7,000/s를 처리했습니다. 시작점의 1.75배지만, 투입한 8,000에도 깔끔한 두 배에도 여전히 못 미칩니다. 출시 때 새 테이블에서 초당 약 4,000건이 넘는 쓰기가 필요하다면, 미리 예열하거나 최대 온디맨드 처리량을 명시적으로 설정하세요 — 증가 메커니즘은 실재하지만 즉각적이지도 않고 여러분의 일정에 너그럽지도 않습니다. 이 동작은 안정적이기도 합니다. 2019년 연구에서는 9,000/초의 부하를 받은 테이블이 테스트 종료 시점에 약 7,000/초까지 성장해 있었습니다 — 우리 테이블이 7년 뒤에 도달한 것과 같은 고원입니다. 그 실행에는 $12.67이 들었고, 그날 저희가 한 일 중 가장 비쌌습니다.
클라우드 서비스를 직접 측정할 때 그대로 옮겨 쓸 수 있는 것들
- 부하 생성기를 대상과 같은 리전에 두세요. 그러지 않으면 서비스가 아니라 여러분의 경로를 측정하는 것입니다.
- Node 프로세스 하나는 메모리와 무관하게 초당 서명 요청 약 2,000건에서 천장을 칩니다. 부하를 러너들로 나누고 실제로 센 결과를 합산하세요.
- 측정 경로에서는 모든 계층의 재시도를 끄세요. 재시도는 벤치마크가 보려고 존재하는 바로 그것을 감추려고 존재합니다.
- 긴 실행 내내 침묵하는 연결을 붙들고 있지 마세요. 비동기로 호출하고, 결과는 대역 외로 전달하고, 폴링하세요.
- 모든 요청에 타임아웃을 주고, 모든 러너에는 막힌 상태의 스냅샷과 함께 부분 데이터를 반환하는 워치독을 주세요.
- 러너마다 연산 횟수의 하드 상한을 두어 페이싱 버그가 청구서를 불리는 대신 중단되게 하고, 실행이 만든 모든 것 — 테이블, 역할, 함수, 로그 — 을
finally에서 정리하세요.
이 데이터가 뒷받침하는 참고 페이지
전체 데이터셋 — 모든 구간, 모든 러너, 지연 시간 백분위, 원문 그대로의 오류 문자열 — 은 이제 저희 DynamoDB 제한 참고 자료의 측정 표를, 앞서 공개한 항목 크기와 페이지 상한 탐색과 나란히 뒷받침합니다. DynamoDB를 매일 다룬다면 DynoTable이 저희가 만든 데스크톱 클라이언트입니다 — 같은 팀이고, 주장을 되풀이하기 전에 라이브 서비스에 확인해 보는 같은 습관을 가졌습니다.