자원 예산 계약 — DB 연결 수·timeout·worker 배치·Edge/Vercel 함수 한도·인입 크기 (이슈 #1332, B02)
한 문장 — 앱·Edge 함수·worker·관리자가 나눠 쓰는 자원의 현재 값을 어디서 왔는지와 함께 적은 표다. 실측한 값(Production 읽기 전용 프로브), 코드에 적힌 값, 플랫폼 문서의 값, 아직 달성 못 한 목표, 재지 않은 값을 구분한다. 재지 않은 동시 처리량은 보장하지 않는다. 상한을 넓히는 것은 오너 결정(전역 지침 §26 ③)이고, 코드 상수와 이 표를 같은 PR 에서 바꾼다.
- 이슈: #1332 (계획 ID B02). 코드:
src/react/services/appResourceContract.tsRELEASE_RESOURCE_CONTRACT(이 표와 같은 값). 증거:scripts/resources/evidence/production-2026-09-07.json(프로브scripts/resources/probe-production.mjs, 2026-09-07T10:44:12Z). 대조:tests/react/resourceContract.test.mjs(코드 ↔ 증거 ↔ 이 문서). - 누가 쓰나: D08(통계 worker 의 lease/fence·timeout), I01(인입 parser 의 메모리·시간·파일 크기), R04(혼합 부하 합격 기준), D11(worker 라운드 목표). G04 성능 예산의 항목·배율은 그대로다 — 이 문서는 그 옆에 놓이는 자원 쪽 표다.
- 다시 재는 법:
node scripts/resources/probe-production.mjs(supabase CLI 로그인 토큰, 쓰기 없음) → 새 evidence 파일 → 코드 상수·이 표를 같이 고친다. 테스트가 세 곳의 값이 다르면 실패한다.
1. 읽는 법 — 유저 A 의 저장 하나로
유저 A 가 세트를 저장하면 앱은 authenticated 역할로 RPC 를 부른다. 그 문장은 8초 안에 끝나야 한다(역할별 statement_timeout). 저장이 끝나면 통계 잡이 큐에 들어가고, 매분 도는 cron 이 잡을 25개씩 집어 처리한다 — 이 라운드는 postgres 역할이라 120초 상한을 받는다. G04 실측에서 10년치 기록을 가진 사용자 한 명의 라운드가 127초였다. 즉 지금은 상한과 같은 크기이고, 그래서 "라운드 60초 이하" 는 목표(D04·D08 이 달성)이지 현재 값이 아니다. 연결은 컴퓨트 Micro 가 직접 60개(관리자 예약 3개 제외 57개)·풀러 200개를 주고, 프로브 시점에 앱 쪽 연결은 4개였다.
2. 상태의 뜻
| status | 뜻 | 근거 형식 |
|---|---|---|
measured | Production 에서 읽기 전용 프로브로 실측 | evidence JSON 의 같은 값 |
code | 레포 코드의 상수 | 파일 경로·상수 이름 |
documented | 플랫폼 공식 문서의 한도. 우리가 재지 않았다 | 인용 URL |
target | 이번 릴리스 목표. 달성·검증 전 | 달성 담당 ID |
unmeasured | 값 없음(null). 보장하지 않는다 | 누가 잴지 |
3. 계약 표
3-1. Postgres (Supabase 컴퓨트 Micro · 조직 플랜 pro · Postgres 17.6.1.127)
| 키 | 값 | 단위 | status | 근거 | 소비자 |
|---|---|---|---|---|---|
| computeVariant | ci_micro | variant | measured | Management API billing/addons — Micro: 2 vCPU(공유)·1GB | R04 |
| directConnectionsMax | 60 | connections | measured | pg_settings max_connections = 컴퓨트 Micro connections_direct | R04, D08 |
| superuserReservedConnections | 3 | connections | measured | pg_settings superuser_reserved_connections | R04 |
| directConnectionsUsable | 57 | connections | measured | max_connections − superuser_reserved_connections. 앱·Edge·cron·관리 도구가 나눠 쓴다 | R04, D08 |
| poolerClientConnectionsMax | 200 | connections | measured | 컴퓨트 Micro connections_pooler (pooler max_client_conn 은 null = 등급 기본값) | R04 |
| poolerMode | transaction | mode | measured | config/database/pooler pool_mode (port 6543) | R04, D08 |
| poolerDefaultPoolSize | — | connections | unmeasured | pooler default_pool_size = null (등급 기본값, API 가 숫자를 주지 않음) | R04 |
| statementTimeoutAnonMs | 3000 | ms | measured | pg_db_role_setting anon | R04 |
| statementTimeoutAuthenticatedMs | 8000 | ms | measured | pg_db_role_setting authenticated — 앱 화면 RPC 의 실제 상한 | R04, D08, I01 |
| statementTimeoutServiceRoleMs | 120000 | ms | measured | pg_db_role_setting service_role — Edge 함수·관리자 대리 경로 | D08, I01 |
| statementTimeoutServerMs | 120000 | ms | measured | pg_settings statement_timeout — cron(postgres 역할)이 상속 | D08 |
| lockTimeoutMs | 0 | ms | measured | pg_settings lock_timeout = 0 (무제한; authenticator 역할만 8s) | D08 |
| idleInTransactionSessionTimeoutMs | 0 | ms | measured | pg_settings = 0 (무제한). 열린 트랜잭션이 연결을 붙들어도 서버가 끊지 않는다 | D08, R04 |
| workMemKb | 3500 | kB | measured | pg_settings work_mem | R04 |
| sharedBuffersMb | 256 | MB | measured | pg_settings shared_buffers | R04 |
| effectiveCacheSizeMb | 768 | MB | measured | pg_settings effective_cache_size | R04 |
| maxWorkerProcesses | 6 | processes | measured | pg_settings max_worker_processes (pg_cron·autovacuum·parallel 공유) | D08 |
| maxParallelWorkers | 2 | workers | measured | pg_settings max_parallel_workers | D08 |
| clientBackendsObserved | 4 | connections | measured | pg_stat_activity client backend — 프로브 시점 1회 관측(postgrest·mgmt-api·postgres_exporter·기타). 상한이 아니다 | R04 |
3-2. 통계·정리 worker (pg_cron + Edge)
| 키 | 값 | 단위 | status | 근거 | 소비자 |
|---|---|---|---|---|---|
| statsRefreshCronSchedule | * * * * * | cron | measured | cron.job lift-guild-stats-refresh | D08, R04 |
| statsRefreshCronBatch | 25 | jobs/run | measured | run_user_exercise_stats_refresh_cron(25) | D08, R04 |
| statsBackfillScanBatch | 50 | owners/run | measured | barbelic-stats-backfill-scan 매시 0분, run_user_exercise_stats_backfill_scan_cron(50) | D08 |
| prOverviewRolloverBatch | 10 | owners/run | measured | lift-guild-pr-overview-snapshot-rollover 매분, run_user_pr_overview_snapshot_rollover(10) | D08 |
| statsEdgeProcessLimit | 10 | jobs/call | code | supabase/functions/stats-process-refresh-jobs/index.ts body.limit 기본 10, 최대 100 | D08 |
| statsEdgeEnqueueLimit | 50 | owners/call | code | 같은 파일 enqueue_limit 기본 50, 최대 500 | D08 |
| wodupImportEdgeBatchLimit | 5 | batches/call | code | supabase/functions/wodup-process-import-jobs/index.ts limit 1..5 (기본 1) | I01 |
| workerRoundStatementTimeoutMs | 120000 | ms | measured | cron 잡은 postgres 역할 → 서버 statement_timeout 120s 상속. G04 실측 10년 프로필 라운드 127,617ms — 이미 이 상한 크기 | D08 |
| workerRoundTargetMs | 60000 | ms | target | 다음 분의 cron 과 겹치지 않도록 60s 이하 — D04(영향 범위만 재계산)·D08(lease/fence)이 달성. 현재 값이 아니다 | D08, D11 |
| workerConcurrency | — | workers | unmeasured | cron 잡 겹침·Edge 함수 동시 호출 수는 재지 않았다 — D08 이 lease/fence 로 정하고 R04 가 부하에서 잰다 | D08, R04 |
3-3. Supabase Edge Functions (Deno) — 문서 인용
| 키 | 값 | 단위 | status | 근거 | 소비자 |
|---|---|---|---|---|---|
| memoryMb | 256 | MB | documented | https://supabase.com/docs/guides/functions/limits Maximum Memory | I01, D08 |
| wallClockMs | 400000 | ms | documented | 같은 문서 Paid plans 400s (조직 플랜 pro 실측) | I01, D08 |
| cpuTimeMs | 2000 | ms | documented | 같은 문서 Maximum CPU Time 2s — parse 같은 CPU 작업은 2초 안에 | I01, D08 |
| requestIdleTimeoutMs | 150000 | ms | documented | 같은 문서 Request idle timeout | I01 |
| bundleSizeMb | 20 | MB | documented | 같은 문서 Function size (local bundling) | B02 |
| requestBodyBytes | — | bytes | unmeasured | 문서에 명시 없음 | I01 |
3-4. Vercel Functions (api/**) — 문서 인용, Hobby
| 키 | 값 | 단위 | status | 근거 | 소비자 |
|---|---|---|---|---|---|
| maxDurationMs | 300000 | ms | documented | https://vercel.com/docs/functions/limitations Fluid compute Hobby 300s 기본=최대 (프로젝트 생성 2026-05, Fluid 기본; 대시보드 미확인) | R04 |
| memoryMb | 2048 | MB | documented | 같은 문서 Hobby 2GB / 1 vCPU | R04 |
| requestBodyBytes | 4500000 | bytes | documented | 같은 문서 request/response body 4.5MB (413) | I01, R04 |
| adminRequestBodyBytes | 4096 | bytes | code | api/auth/_shared.js BODY_LIMIT_BYTES(readJsonBody) — 관리자·계정 API 공용, 초과 시 413 뒤 연결 종료(#1331) | R04 |
| testAdminRequestBodyBytes | 4096 | bytes | code | api/auth/test-admin/session.js — 같은 readJsonBody 기본 상한(#1331 뒤 별도 2048 상한 없음) | R04 |
| fileDescriptors | 1024 | descriptors | documented | 같은 문서 — 동시 실행이 공유 | R04 |
3-5. 클라이언트 인입 파일 (브라우저 parse)
| 키 | 값 | 단위 | status | 근거 | 소비자 |
|---|---|---|---|---|---|
| motraFileBytes | 31457280 | bytes | code | features/import/importContract.ts IMPORT_LIMITS.motra.maxFileBytes (30MiB; 관리자 파서 사본은 Barbelic-docs admin) | I01 |
| inbodyFileBytes | 20971520 | bytes | code | 같은 파일 IMPORT_LIMITS.inbody.maxFileBytes (20MiB) | I01 |
| wodupJsonlMaxLines | 10000 | lines | code | 같은 파일 IMPORT_LIMITS.wodup.maxSampleLines | I01 |
| parseMemoryBytes | 54945496 | bytes | measured | I01(#1415) 2026-09-10 scripts/performance/import/wodup-large-file.mjs(npm run perf:import -- --sessions 10000): 10,000세션·24.3MB 파일(표본 1만 줄 한도에 닿는 대표 큰 파일)의 브라우저 사전검사 동안 힙 최고점 증가, Node 24 win32 3회 최댓값. 완료 뒤 남는 양 0.2MB. 2,000세션(4.9MB)은 12.5MB. 표본 한도(32MB·1만 줄)가 상한 | I01, R04 |
| preflightMaxSyncBlockMs | 47 | ms | measured | 같은 계측 — 500줄마다 양보하는 사전검사가 이벤트 루프를 한 번에 멈춘 최대 시간(24.3MB 표본 문자열을 줄로 쪼개는 한 번이 가장 길다). 전체 사전검사 0.29~0.33초. 브라우저 실측 아님(Node) | I01, R04 |
| workerNormalizeHeapPeakBytes | 225966552 | bytes | measured | 같은 계측 — worker 정규화(normalizeWodupJsonl, 동기 0.92~0.94초) 동안 힙 최고점 증가; 산출 81MB·세트 12만. Edge 함수 메모리 256MB 의 88%(CPU 0.94초는 2초 한도 안). 2,000세션은 52MB·0.2초. 파일 전체를 한 문자열로 들어 세션 수에 비례 — 줄 단위 스트리밍은 D09 runtime 과 함께 후속 | I01, D09, R04 |
4. 인계 — 누가 어느 값을 읽나
| 담당 | 읽는 값 |
|---|---|
| D08 | 통계 worker 의 lease/fence·timeout 설계: statementTimeoutServiceRoleMs·statementTimeoutServerMs·workerRoundStatementTimeoutMs·workerRoundTargetMs·statsRefreshCronBatch·maxWorkerProcesses·idleInTransactionSessionTimeoutMs |
| I01 | 인입 parser 의 자원 사용 계약: edgeFunctions.cpuTimeMs·memoryMb·wallClockMs·clientImport.*·vercelFunctions.requestBodyBytes·wodupImportEdgeBatchLimit |
| R04 | 혼합 부하 합격: directConnectionsUsable·poolerClientConnectionsMax·statementTimeoutAuthenticatedMs·vercelFunctions.*·clientBackendsObserved(기준 관측) |
| D11 | 통계 세대 개선의 목표: workerRoundTargetMs |
5. 이 표에서 하지 않은 것 (숫자로 쓰지 않은 것)
- 동시 처리량(초당 저장 수, 동시 사용자 수)은 재지 않았다 — R04 의 혼합 부하 시험이 G04 절차로 잰다. 여기 있는 연결 수·timeout 은 그 시험이 넘지 말아야 할 경계이지 처리량 보장이 아니다.
- Vercel 프로젝트가 실제로 Fluid compute 인지, 요금제가 여전히 Hobby 인지는 대시보드에서만 보인다(오너 손 목록). 표의 값은 그 전제로 인용했다.
- Edge 함수의 요청 본문 한도는 문서에 없다. Wodup 인입은 파일을 Storage 에 올리고 Edge 는 URL 만 받으므로 지금 경로에서는 걸리지 않는다.
- G04 예산의 상한은 넓히지 않았다.
workerRoundTargetMs는 그 예산보다 낮은 목표다.