AWS Batch · 2026-11-07 · Batch arc

AWS Batch for MCP Servers: Compute Environments, Job Lifecycle, Container Properties, and Array Jobs

AliveMCP monitors MCP endpoints every 60 seconds, and among the AWS-integrated servers we probe, Batch misconfiguration clusters around four failure modes that catch nearly every team on first contact: a server that calls RegisterJobDefinitionCommand and then passes the job definition name (without the :N revision suffix) to every subsequent SubmitJobCommand, not realizing that each RegisterJobDefinitionCommand creates a new revision and the unqualified name silently resolves to whatever revision number Batch considers "latest" — when a container image is updated and a new revision registered, old jobs already in RUNNABLE state can start on the wrong revision depending on scheduler timing, causing inconsistent behavior in the same batch run; a server that creates an EC2 compute environment and passes the IAM role ARN (arn:aws:iam::...:role/batch-instance-role) as the instanceRole field instead of the instance profile ARN (arn:aws:iam::...:instance-profile/batch-instance-role) — the API accepts either without error, but EC2 instances launched by Batch will fail to register with ECS and all jobs will stay in RUNNABLE state indefinitely without a clear error message; a server that uses CancelJobCommand on a job that has already moved from RUNNABLE to STARTING, not realizing that cancel only affects pre-run states (SUBMITTED, PENDING, RUNNABLE) — once a job reaches STARTING, CancelJobCommand silently succeeds but has no effect and the container continues to run, causing unintended duplicate execution when the tool retries the cancel; and a server that submits array jobs using arrayProperties:{size:500} but sets a retryStrategy.attempts:5 with no evaluateOnExit rules, so every failed child — including those that failed due to Spot interruption — retries five times on what might be an application logic error, burning compute budget on jobs that would have succeeded on retry but weren't interrupted at all, while simultaneously missing Spot interruption retries on jobs whose parent never reaches SUCCEEDED because a single stubborn non-Spot child exhausted its retry budget on a deterministic error. Four AWS Batch patterns form the complete batch compute integration stack for MCP server builders. Compute environment and job queue setup with CreateComputeEnvironmentCommand, CreateJobQueueCommand, and RegisterJobDefinitionCommand is the infrastructure layer: you provision the fleet model (EC2, Spot, Fargate, or Fargate Spot), configure job queues with priority-ordered compute environments, register versioned job definitions that describe the container image and resource requirements, and submit jobs with SubmitJobCommand. Job lifecycle management with DescribeJobsCommand, ListJobsCommand, CancelJobCommand, and TerminateJobCommand is the operational layer: you poll jobs through the seven-state machine, fetch CloudWatch log streams on failure, and configure evaluateOnExit rules to retry on Spot interruption without retrying deterministic application errors. Container properties with resourceRequirements, secrets, EFS volume mounts, and logConfiguration are the runtime layer that determines what your container can access and how much it costs — the IAM split between jobRoleArn (what your code calls) and executionRoleArn (what Fargate infrastructure needs) is the most common source of AccessDenied in Batch. Array jobs, job dependencies, and fair-share scheduling scale your batch pipelines: a single SubmitJobCommand fans out to N parallel child jobs via arrayProperties.size, downstream jobs can depend on upstream arrays with no-type (all-succeed aggregation), SEQUENTIAL, or N_TO_N index-matched coupling, and CreateSchedulingPolicyCommand distributes capacity proportionally across teams sharing a queue. This guide synthesizes all four Batch patterns into a complete batch compute integration stack for MCP server builders.

TL;DR

Pattern 1: Compute environments, job queues, and job definitions

When an MCP server tool needs to run containerized batch workloads without managing EC2 instances, AWS Batch infrastructure setup is the starting point. The core model is: provision a compute environment that defines the underlying fleet, attach one or more job queues that route jobs to environments by priority, register job definitions that version-stamp the container spec, then submit jobs with SubmitJobCommand.

Installing the SDK and creating the client

AWS Batch uses a dedicated SDK package. Install it once — it covers all Batch resources (compute environments, queues, definitions, and jobs).

npm install @aws-sdk/client-batch

import {
  BatchClient,
  CreateComputeEnvironmentCommand,
  CreateJobQueueCommand,
  RegisterJobDefinitionCommand,
  SubmitJobCommand,
  DescribeComputeEnvironmentsCommand,
} from '@aws-sdk/client-batch';

const batch = new BatchClient({ region: process.env.AWS_REGION ?? 'us-east-1' });

Construct the client once at module initialization — it is stateless and safe to share across concurrent tool calls. Batch is a regional service; use the region where your VPC subnets live to avoid cross-region networking costs.

Compute environment types

The type inside computeResources determines the underlying fleet model:

typeUnderlying fleetWhen to use
FARGATEFargate on-demandSimple serverless workloads; no instanceRole required
FARGATE_SPOTFargate SpotNon-critical Fargate jobs at ~70% savings
EC2On-demand EC2GPU workloads, privileged containers, host-path volumes
SPOTEC2 Spot InstancesCost-sensitive EC2 workloads with retry logic
// Fargate compute environment — simplest setup
export async function createFargateComputeEnv(params: {
  name: string;
  maxvCpus: number;
  subnetIds: string[];
  securityGroupIds: string[];
}) {
  const response = await batch.send(new CreateComputeEnvironmentCommand({
    computeEnvironmentName: params.name,
    type: 'MANAGED',
    state: 'ENABLED',
    computeResources: {
      type: 'FARGATE',
      maxvCpus: params.maxvCpus,
      subnets: params.subnetIds,
      securityGroupIds: params.securityGroupIds,
    },
    // serviceRole omitted → Batch uses AWSServiceRoleForBatch service-linked role automatically
  }));
  return response.computeEnvironmentArn!;
}

// EC2 compute environment — instanceRole must be an INSTANCE PROFILE ARN, not the role ARN
export async function createEc2ComputeEnv(params: {
  name: string;
  minvCpus: number;      // 0 = scale to zero when idle
  maxvCpus: number;
  instanceTypes: string[];  // ['optimal'] or specific: ['m5.xlarge', 'm6i.xlarge']
  subnetIds: string[];
  securityGroupIds: string[];
  instanceRole: string;  // arn:aws:iam::123456789012:instance-profile/batch-instance-profile
}) {
  const response = await batch.send(new CreateComputeEnvironmentCommand({
    computeEnvironmentName: params.name,
    type: 'MANAGED',
    state: 'ENABLED',
    computeResources: {
      type: 'EC2',
      minvCpus: params.minvCpus,
      maxvCpus: params.maxvCpus,
      instanceTypes: params.instanceTypes,
      subnets: params.subnetIds,
      securityGroupIds: params.securityGroupIds,
      instanceRole: params.instanceRole, // PROFILE ARN, not role ARN — silent bug if wrong
    },
  }));
  return response.computeEnvironmentArn!;
}

After creation, the compute environment enters CREATING state and transitions to VALID (healthy) or INVALID (misconfigured). Poll before attaching to a job queue:

export async function waitForComputeEnvValid(name: string): Promise<void> {
  for (let i = 0; i < 60; i++) {
    const r = await batch.send(new DescribeComputeEnvironmentsCommand({
      computeEnvironments: [name],
    }));
    const env = r.computeEnvironments?.[0];
    if (env?.status === 'VALID')   return;
    if (env?.status === 'INVALID') throw new Error(`CE invalid: ${env.statusReason}`);
    await new Promise(r => setTimeout(r, 5_000));
  }
  throw new Error('Compute environment did not become VALID within 5 minutes');
}

Job queues and multi-environment Spot fallback

A job queue connects to one or more compute environments with a priority-ordered list. When Batch schedules a job, it tries the highest-priority (lowest order number) environment first; if that environment is at capacity, it falls over to lower-priority ones. This pattern is the standard way to get Spot savings with on-demand reliability:

// Spot first (order:1), on-demand fallback (order:2)
export async function createJobQueue(params: {
  name: string;
  primaryEnvArn: string;    // Spot or Fargate Spot
  fallbackEnvArn?: string;  // On-demand fallback
  priority?: number;        // Queue-level priority (higher = preferred when multiple queues exist)
}) {
  const response = await batch.send(new CreateJobQueueCommand({
    jobQueueName: params.name,
    state: 'ENABLED',
    priority: params.priority ?? 1,
    computeEnvironmentOrder: [
      { order: 1, computeEnvironment: params.primaryEnvArn },
      ...(params.fallbackEnvArn
        ? [{ order: 2, computeEnvironment: params.fallbackEnvArn }]
        : []),
    ],
  }));
  return response.jobQueueArn!;
}

Job definitions and revision pinning

RegisterJobDefinitionCommand creates a versioned template. Each call increments the revision number (name:1, name:2, name:3, …). Never pass the bare name to SubmitJobCommand — always pin to name:N to guarantee all jobs in a batch use the same container image and configuration:

export async function registerJobDef(params: {
  name: string;
  image: string;
  vcpus: number;
  memoryMiB: number;
  command?: string[];
  jobRoleArn?: string;
  executionRoleArn?: string; // Required for Fargate
  useFargate?: boolean;
}) {
  const response = await batch.send(new RegisterJobDefinitionCommand({
    jobDefinitionName: params.name,
    type: 'container',
    containerProperties: {
      image: params.image,
      resourceRequirements: [
        { type: 'VCPU',   value: String(params.vcpus) },
        { type: 'MEMORY', value: String(params.memoryMiB) },
      ],
      ...(params.command          ? { command:          params.command }          : {}),
      ...(params.jobRoleArn       ? { jobRoleArn:       params.jobRoleArn }       : {}),
      ...(params.executionRoleArn ? { executionRoleArn: params.executionRoleArn } : {}),
      ...(params.useFargate ? {
        fargatePlatformConfiguration: { platformVersion: 'LATEST' },
        networkConfiguration:         { assignPublicIp: 'ENABLED' },
      } : {}),
      logConfiguration: {
        logDriver: 'awslogs',
        options: {
          'awslogs-group':         '/aws/batch/job',
          'awslogs-region':        'us-east-1',
          'awslogs-stream-prefix': params.name,
        },
      },
    },
    retryStrategy: { attempts: 3 },
    platformCapabilities: [params.useFargate ? 'FARGATE' : 'EC2'],
  }));

  // Always store and use the name:revision form in SubmitJobCommand
  return `${params.name}:${response.revision}`;
}

Pattern 2: Job lifecycle — submit, poll, cancel, and retry strategy

AWS Batch job lifecycle covers everything that happens after SubmitJobCommand returns. Jobs move through a seven-state machine, and your MCP tool must poll through it before reporting success or failure to the agent calling the tool.

The seven-state job machine

StateMeaningTypical durationMCP action
SUBMITTEDAccepted, waiting for scheduler slotSecondsKeep polling
PENDINGWaiting for a dependency job to completeUntil dependency finishesKeep polling
RUNNABLEReady to run, waiting for capacitySeconds to minutesKeep polling; stuck > 5 min → diagnose
STARTINGContainer being pulled and startedSeconds to 2 minutesKeep polling
RUNNINGJob container executingYour workload durationKeep polling
SUCCEEDEDContainer exited 0TerminalSuccess
FAILEDNon-zero exit, timeout, or terminatedTerminalFailure — log statusReason

A job stuck in RUNNABLE is the most common operational incident in Batch. The three causes: (1) maxvCpus too low — the queue has no headroom; (2) compute environment status is INVALID (call DescribeComputeEnvironmentsCommand and read statusReason); (3) requested Spot instance types unavailable in the configured subnets — add more instance types or additional AZs.

Polling with DescribeJobsCommand

import {
  DescribeJobsCommand,
  type JobDetail,
} from '@aws-sdk/client-batch';

export async function waitForJob(
  jobId: string,
  opts: { pollIntervalMs?: number; timeoutMs?: number } = {}
): Promise<JobDetail> {
  const pollInterval = opts.pollIntervalMs ?? 15_000;
  const deadline     = Date.now() + (opts.timeoutMs ?? 60 * 60 * 1000); // 1 hour default

  while (Date.now() < deadline) {
    const r   = await batch.send(new DescribeJobsCommand({ jobs: [jobId] }));
    const job = r.jobs?.[0];
    if (!job) throw new Error(`Job ${jobId} not found`);

    if (job.status === 'SUCCEEDED') return job;
    if (job.status === 'FAILED') {
      const lastAttempt = job.attempts?.at(-1);
      throw new Error(
        `Job ${jobId} failed: ${job.statusReason ?? 'unknown'} ` +
        `(exit code ${lastAttempt?.container?.exitCode ?? 'N/A'})`
      );
    }
    await new Promise(r => setTimeout(r, pollInterval));
  }
  throw new Error(`Job ${jobId} timed out`);
}

DescribeJobsCommand accepts up to 100 job IDs per call. The response includes logStreamName — use it to fetch the full container stdout/stderr from CloudWatch Logs after a failure:

import {
  CloudWatchLogsClient,
  GetLogEventsCommand,
} from '@aws-sdk/client-cloudwatch-logs';

const logs = new CloudWatchLogsClient({ region: 'us-east-1' });

export async function getJobLogs(logStreamName: string): Promise<string[]> {
  const lines: string[] = [];
  let nextForwardToken: string | undefined;

  do {
    const r = await logs.send(new GetLogEventsCommand({
      logGroupName:  '/aws/batch/job',
      logStreamName,
      startFromHead: true,
      nextToken:     nextForwardToken,
    }));
    for (const event of r.events ?? []) lines.push(event.message ?? '');
    // GetLogEvents returns the same token when at the end — that's the stop condition
    if (nextForwardToken === r.nextForwardToken) break;
    nextForwardToken = r.nextForwardToken;
  } while (nextForwardToken);

  return lines;
}

Cancel vs terminate — which one to call

Calling the wrong stop command silently succeeds but has no effect. The rule is simple: cancel for pre-run jobs, terminate for running jobs.

import {
  CancelJobCommand,
  TerminateJobCommand,
} from '@aws-sdk/client-batch';

export async function stopJob(jobId: string, reason: string) {
  const r   = await batch.send(new DescribeJobsCommand({ jobs: [jobId] }));
  const job = r.jobs?.[0];
  if (!job) return;

  const preRunStates  = new Set(['SUBMITTED', 'PENDING', 'RUNNABLE']);
  const runningStates = new Set(['STARTING', 'RUNNING']);

  if (preRunStates.has(job.status!)) {
    // CancelJobCommand: does nothing if job has moved to STARTING or RUNNING
    await batch.send(new CancelJobCommand({ jobId, reason }));
  } else if (runningStates.has(job.status!)) {
    // TerminateJobCommand: sends SIGTERM → 30s grace period → SIGKILL
    await batch.send(new TerminateJobCommand({ jobId, reason }));
  }
  // SUCCEEDED/FAILED: already terminal, no action needed
}

Retry strategy with evaluateOnExit

Without evaluateOnExit, Batch retries on any non-zero exit up to attempts times — including deterministic application errors that will never succeed. evaluateOnExit rules let you retry on Spot interruption and OOM kills while failing fast on application errors. Rules are evaluated in order; first match wins:

import { type RetryStrategy } from '@aws-sdk/client-batch';

const spotAwareRetry: RetryStrategy = {
  attempts: 5,
  evaluateOnExit: [
    {
      // Spot interruption: statusReason contains this phrase
      onStatusReason: 'Host EC2 (*) was terminated',
      action: 'RETRY',
    },
    {
      // OOM kill: Linux kernel sends SIGKILL, container exits 137
      onExitCode: '137',
      action: 'RETRY',
    },
    {
      // Application bug: fail fast, don't waste compute retrying a known bad exit
      onExitCode: '1',
      action: 'EXIT',
    },
    {
      // All other non-zero exits: retry up to attempts limit
      onExitCode: '[2-9]|[1-9][0-9]+',
      action: 'RETRY',
    },
  ],
};

The onStatusReason, onReason, and onExitCode fields support glob-style wildcards (* matches any sequence). onExitCode also accepts regex patterns. If no rule matches, the default is RETRY up to attempts.

Pattern 3: Container properties — resource requirements, Fargate, secrets, and logging

The containerProperties object in RegisterJobDefinitionCommand is where most Batch runtime errors originate. The key decisions are: which resource fields to use (the array vs deprecated top-level), how to split IAM between job and execution roles, and how to inject secrets without exposing them in DescribeJobs output.

Resource requirements — always use the array

The deprecated top-level vcpus and memory fields are not supported on Fargate and produce unexpected behavior on EC2. Always use resourceRequirements as an array. For Fargate, the vCPU and memory values must form a valid combination:

vCPUValid memory values (MiB)
0.25512, 1024, 2048
0.51024–4096 (1024 increments)
12048–8192 (1024 increments)
24096–16384 (1024 increments)
48192–30720 (1024 increments)
816384–61440 (4096 increments)
1632768–122880 (8192 increments)
import { type ContainerProperties } from '@aws-sdk/client-batch';

// Fargate job definition
const fargateContainer: ContainerProperties = {
  image: '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-job:latest',
  resourceRequirements: [
    { type: 'VCPU',   value: '2' },
    { type: 'MEMORY', value: '4096' },
  ],
  jobRoleArn:       'arn:aws:iam::123456789012:role/batch-job-role',       // what your code calls
  executionRoleArn: 'arn:aws:iam::123456789012:role/batch-execution-role', // ECR + logs + secrets
  fargatePlatformConfiguration: { platformVersion: 'LATEST' },
  networkConfiguration: { assignPublicIp: 'ENABLED' }, // DISABLED for private subnet + NAT
};

// GPU job on EC2 — add GPU to resourceRequirements; specify GPU instances in compute env
const gpuContainer: ContainerProperties = {
  image: '763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:2.2.0-gpu-py310',
  resourceRequirements: [
    { type: 'VCPU',   value: '8' },
    { type: 'MEMORY', value: '32768' },
    { type: 'GPU',    value: '1' }, // EC2 only — Fargate does not support GPU
  ],
  jobRoleArn: 'arn:aws:iam::123456789012:role/batch-ml-job-role',
};

jobRoleArn vs executionRoleArn

FieldAssumed byGrants access toRequired on
jobRoleArnYour container processAWS APIs your code calls (S3, DynamoDB, SQS, etc.)EC2 and Fargate
executionRoleArnECS Fargate agentECR image pull, CloudWatch Logs, Secrets Manager, SSMFargate only (but required)

Both roles must have a trust policy allowing ecs-tasks.amazonaws.com to assume them. The managed policy AmazonECSTaskExecutionRolePolicy covers ECR and CloudWatch Logs for the execution role; add secretsmanager:GetSecretValue and ssm:GetParameters manually if you use the secrets array.

Secrets injection — never use the environment array for secrets

Values in the environment array are plaintext — visible in DescribeJobs API responses and in the AWS console. Use the secrets array to inject values from Secrets Manager or Parameter Store; Batch fetches and injects them before the container process starts:

const containerWithSecrets: ContainerProperties = {
  image: '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-job:latest',
  resourceRequirements: [
    { type: 'VCPU',   value: '1' },
    { type: 'MEMORY', value: '2048' },
  ],
  // Non-sensitive config — safe to log and inspect in DescribeJobs
  environment: [
    { name: 'ENVIRONMENT',   value: 'production' },
    { name: 'OUTPUT_BUCKET', value: 'my-results-bucket' },
  ],
  // Secrets — injected at job startup, NOT visible in DescribeJobs output
  secrets: [
    {
      // Secrets Manager: inject a specific key from a JSON secret
      // Format: arn:...:secret:name:json-key::  (note the trailing ::)
      name:      'DB_PASSWORD',
      valueFrom: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/db-AbCdEf:password::',
    },
    {
      // SSM Parameter Store (SecureString)
      name:      'API_KEY',
      valueFrom: 'arn:aws:ssm:us-east-1:123456789012:parameter/prod/api-key',
    },
  ],
  jobRoleArn:       'arn:aws:iam::123456789012:role/batch-job-role',
  executionRoleArn: 'arn:aws:iam::123456789012:role/batch-execution-role',
};

EFS volumes for shared persistent storage

EFS volumes are available on both EC2 and Fargate and are the standard choice for sharing data between batch jobs or persisting results across runs:

const containerWithEfs: ContainerProperties = {
  image: '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-job:latest',
  resourceRequirements: [
    { type: 'VCPU',   value: '2' },
    { type: 'MEMORY', value: '4096' },
  ],
  volumes: [{
    name: 'efs-data',
    efsVolumeConfiguration: {
      fileSystemId:        'fs-0abc1234def5678',
      rootDirectory:       '/batch-data',
      transitEncryption:   'ENABLED',
      authorizationConfig: {
        accessPointId: 'fsap-0abc1234def5678',
        iam: 'ENABLED', // use jobRoleArn for EFS authorization
      },
    },
  }],
  mountPoints: [{
    containerPath: '/data',
    sourceVolume:  'efs-data',
    readOnly: false,
  }],
  jobRoleArn:       'arn:aws:iam::123456789012:role/batch-job-role',
  executionRoleArn: 'arn:aws:iam::123456789012:role/batch-execution-role',
  fargatePlatformConfiguration: { platformVersion: 'LATEST' },
  networkConfiguration: { assignPublicIp: 'DISABLED' }, // private subnet with NAT
};

When iam:'ENABLED' is set, the EFS file system uses IAM authorization. Add elasticfilesystem:ClientMount and elasticfilesystem:ClientWrite to the job role for the specific EFS file system ARN.

Fargate-only restrictions

Several containerProperties fields are EC2-only. Passing them in a Fargate job definition causes ClientException: Fargate does not support X at registration time:

Unsupported on FargateEC2 alternative
privileged: trueUse EC2 compute environment
ulimitsNot configurable on Fargate
devicesNot configurable on Fargate
instanceType overrideFargate manages instance selection
Host-path volumesUse EFS volumes instead
resourceRequirements[GPU]Use EC2 p3/g4dn/g5 instance types

Pattern 4: Array jobs, pipeline dependencies, and fair-share scheduling

Array jobs and job dependencies transform Batch from a single-job runner into a parallel processing platform. A single SubmitJobCommand fans out to N identical child jobs; downstream jobs can declare dependencies on upstream arrays; and fair-share scheduling policies distribute capacity proportionally when multiple teams share a queue.

Array jobs — fan-out pattern

Add arrayProperties:{size:N} to SubmitJobCommand. Batch creates N child jobs, each receiving AWS_BATCH_JOB_ARRAY_INDEX (0 to N-1) as an injected environment variable. Your container uses this index to select its slice of work from a pre-generated manifest:

// Submit: fan out to 500 parallel child jobs
const parentJobId = (await batch.send(new SubmitJobCommand({
  jobName:       'daily-etl-2026-11-07',
  jobQueue:      'etl-queue',
  jobDefinition: 'file-processor:12', // pin to specific revision
  arrayProperties: { size: 500 },
  containerOverrides: {
    environment: [
      { name: 'S3_BUCKET', value: 'my-data-bucket' },
      { name: 'MANIFEST',  value: 's3://my-data-bucket/manifests/2026-11-07.json' },
    ],
  },
}))).jobId!;

// In your container code — read the manifest and process only your slice
const index   = parseInt(process.env.AWS_BATCH_JOB_ARRAY_INDEX ?? '0', 10);
const manifest = JSON.parse(await readS3Object(process.env.MANIFEST!));
const myKey   = manifest.keys[index];  // each child processes exactly one key

Child job IDs are {parentId}:0 through {parentId}:N-1. Poll the parent job ID to get aggregate progress:

export async function getArrayProgress(parentJobId: string) {
  const r   = await batch.send(new DescribeJobsCommand({ jobs: [parentJobId] }));
  const job = r.jobs?.[0];
  if (!job) throw new Error(`Parent job ${parentJobId} not found`);

  const summary = job.arrayProperties?.statusSummary ?? {};
  const size    = job.arrayProperties?.size ?? 0;
  return {
    parentStatus: job.status,
    size,
    succeeded:    summary['SUCCEEDED'] ?? 0,
    failed:       summary['FAILED']    ?? 0,
    running:      summary['RUNNING']   ?? 0,
    pctComplete:  size > 0
      ? Math.round(((summary['SUCCEEDED'] ?? 0) / size) * 100)
      : 0,
  };
}

Job dependencies — three dependency types

dependsOn typeSemanticsWhen to use
(no type)Downstream starts only when ALL upstream children succeedAggregation job after fan-out
SEQUENTIALChild N+1 starts only after child N succeeds (serial)Serial pipeline where each step builds on previous output
N_TO_NChild K of downstream starts when child K of upstream succeedsMulti-stage processing of the same dataset with index-matched coupling
// Pattern 1: Aggregation after fan-out
// Step 1: 500 extractors in parallel
const extractId = (await batch.send(new SubmitJobCommand({
  jobName: 'extract', jobQueue: 'etl-queue',
  jobDefinition: 'extractor:5',
  arrayProperties: { size: 500 },
}))).jobId!;

// Step 2: single aggregator waits for ALL 500 extractors
const aggregateId = (await batch.send(new SubmitJobCommand({
  jobName: 'aggregate', jobQueue: 'etl-queue',
  jobDefinition: 'aggregator:3',
  dependsOn: [{ jobId: extractId }], // no type = all-succeed gate
}))).jobId!;

// Pattern 2: N_TO_N — index-matched two-stage pipeline
const transformId = (await batch.send(new SubmitJobCommand({
  jobName: 'transform', jobQueue: 'etl-queue',
  jobDefinition: 'transformer:2', arrayProperties: { size: 200 },
}))).jobId!;

// Validate child K starts only when transform child K succeeds
const validateId = (await batch.send(new SubmitJobCommand({
  jobName: 'validate', jobQueue: 'etl-queue',
  jobDefinition: 'validator:1',
  arrayProperties: { size: 200 }, // must match upstream size for N_TO_N
  dependsOn: [{ jobId: transformId, type: 'N_TO_N' }],
}))).jobId!;

Fair-share scheduling for multi-team queues

When multiple teams share a job queue, fair-share scheduling distributes capacity proportionally based on weightFactor. The scheduler computes each identifier's recent usage, decays old usage by shareDecaySeconds, and gives preference to whichever identifier has consumed less of its weighted share:

import {
  CreateSchedulingPolicyCommand,
  UpdateJobQueueCommand,
} from '@aws-sdk/client-batch';

// Create the fair-share policy
const policyArn = (await batch.send(new CreateSchedulingPolicyCommand({
  name: 'ml-team-fairshare',
  fairsharePolicy: {
    shareDecaySeconds: 300,  // 5-min decay: reactive but not thrashing
    computeReservation: 0,   // 0% reserved for jobs without a shareIdentifier
    shares: [
      { shareIdentifier: 'team-search',   weightFactor: 2.0 }, // double priority
      { shareIdentifier: 'team-recs',     weightFactor: 1.0 }, // baseline
      { shareIdentifier: 'team-platform', weightFactor: 0.5 }, // half baseline
    ],
  },
}))).schedulingPolicyArn!;

// Attach to an existing job queue
await batch.send(new UpdateJobQueueCommand({
  jobQueue:            'ml-training-queue',
  schedulingPolicyArn: policyArn,
}));

// Tag each job submission with a shareIdentifier
await batch.send(new SubmitJobCommand({
  jobName:                 'nightly-training',
  jobQueue:                'ml-training-queue',
  jobDefinition:           'model-trainer:8',
  shareIdentifier:         'team-search',
  schedulingPriorityOverride: 5, // 1–9999: higher = scheduled first within the share
}));

Related pages