@@ -59,6 +59,24 @@ export interface Job {
5959const jobs = new Map < string , Job > ( ) ;
6060const JOB_TTL_MS = 60 * 60 * 1000 ;
6161
62+ // Dynamic concurrency control for video processing.
63+ //
64+ // Instead of a manual counter (which drifted and caused permanent "server busy"
65+ // errors), active process count is derived from actual job state in the map.
66+ //
67+ // Concurrency limit is determined by:
68+ // 1. MEDIA_SERVER_MAX_CONCURRENT_VIDEO_PROCESSES env var (if set, used as ceiling)
69+ // 2. Otherwise: floor(cpuCount / 2), minimum 1
70+ // 3. Dynamically reduced when CPU load or process memory is high
71+ //
72+ // CPU throttling: when 1-minute load average per core exceeds 0.8,
73+ // effective max is scaled down proportionally.
74+ //
75+ // Memory throttling (opt-in): set MEDIA_SERVER_MEMORY_LIMIT_MB to the container's
76+ // memory limit. When process RSS exceeds 85% of that limit, effective max is reduced.
77+ // Uses process-level RSS (not system-wide free memory) so it works correctly on
78+ // shared hosts where os.freemem() reflects other tenants.
79+
6280const configuredMaxProcesses =
6381 Number . parseInt (
6482 process . env . MEDIA_SERVER_MAX_CONCURRENT_VIDEO_PROCESSES ?? "0" ,
@@ -75,6 +93,7 @@ function isActivePhase(phase: JobPhase): boolean {
7593 return phase !== "complete" && phase !== "error" && phase !== "cancelled" ;
7694}
7795
96+ // Derived from actual job state — no manual increment/decrement that can drift
7897export function getActiveVideoProcessCount ( ) : number {
7998 let count = 0 ;
8099 for ( const job of jobs . values ( ) ) {
0 commit comments