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
- Compute environments and job definitions — install
@aws-sdk/client-batch; constructBatchClient({region})once; create a managed Fargate compute environment withCreateComputeEnvironmentCommand({computeEnvironmentName, type:'MANAGED', computeResources:{type:'FARGATE', maxvCpus, subnets, securityGroupIds}}); for EC2 addinstanceRolepointing to the instance profile ARN not the role ARN; pollDescribeComputeEnvironmentsCommanduntilstatus==='VALID'; create a job queue withCreateJobQueueCommand({jobQueueName, priority:1, computeEnvironmentOrder:[{order:1, computeEnvironment}]}); register a job definition withRegisterJobDefinitionCommand({jobDefinitionName, type:'container', containerProperties:{image, resourceRequirements:[{type:'VCPU',value:'2'},{type:'MEMORY',value:'4096'}]}, platformCapabilities:['FARGATE']})— each call returns a new revision number; always pin toname:revisionin productionSubmitJobCommandcalls: The three fleet choices cover different tradeoffs: Fargate is simplest (no instance management, noinstanceRole), EC2 Spot is cheapest (70–90% savings, requires Spot interruption handling inretryStrategy.evaluateOnExit), and Fargate Spot combines simplicity with ~70% savings. Multi-environment queues give automatic fallback: set order:1 on Spot and order:2 on on-demand — Batch drains Spot capacity first, then spills to on-demand. TheserviceRolefield is optional since 2017; Batch creates the AWSServiceRoleForBatch service-linked role automatically. Cold-start for a new EC2 Spot fleet adds 60–120 seconds to job start time — budget for it intimeout.attemptDurationSecondsso the first job on a cold fleet is not killed before the container even starts. - Job lifecycle — submit with
SubmitJobCommand({jobName, jobQueue, jobDefinition:'name:revision'}); pollDescribeJobsCommand({jobs:[jobId]})every 15 seconds untiljob.statusisSUCCEEDEDorFAILED; onFAILEDlogjob.statusReasonandjob.attempts[last].container.exitCode; retrieve logs withGetLogEventsCommand({logGroupName:'/aws/batch/job', logStreamName:job.logStreamName})looping untilnextForwardTokenstops changing; useCancelJobCommand({jobId, reason})only forSUBMITTED/PENDING/RUNNABLEjobs; forSTARTING/RUNNINGjobs useTerminateJobCommandinstead; setretryStrategy.evaluateOnExitrules so Spot interruption (onStatusReason:'Host EC2 (*) was terminated') and OOM kills (onExitCode:'137') retry, while application errors (onExitCode:'1') exit fast: RUNNABLE-stuck diagnosis: (1)maxvCpuson the compute environment is below the sum of running + pending job resource requests — increase it withUpdateComputeEnvironmentCommand; (2) the compute environment'sstatusisINVALID— callDescribeComputeEnvironmentsCommandand readstatusReason; (3) the requested Spot instance types are not available in any configured subnet — add more instance types (use['optimal']as a catch-all) or add subnets in additional AZs. Settimeout.attemptDurationSecondsinRegisterJobDefinitionCommandas a wall-clock guard per attempt — when the timer expires, Batch sends SIGTERM, waits 30 seconds, then SIGKILL. You can also override per-submit inSubmitJobCommand({timeout:{attemptDurationSeconds}}). - Container properties — always use
resourceRequirements:[{type:'VCPU',value:'2'},{type:'MEMORY',value:'4096'}], not the deprecated top-levelvcpus/memoryfields (required for Fargate, strongly recommended for EC2); for Fargate also addexecutionRoleArn(ECS agent role — needs ECR, CloudWatch Logs, and Secrets Manager permissions),fargatePlatformConfiguration:{platformVersion:'LATEST'}, andnetworkConfiguration:{assignPublicIp:'ENABLED'}for public subnets; put non-sensitive config inenvironment:[{name, value}]; put secrets insecrets:[{name, valueFrom:'arn:aws:secretsmanager:...:secret:...'}]injected at job startup; addlogConfiguration:{logDriver:'awslogs', options:{'awslogs-group':'/aws/batch/my-pipeline','awslogs-region':'us-east-1','awslogs-stream-prefix':'my-pipeline'}}: IAM role split:jobRoleArnis assumed by your container process and grants what your code can call in AWS (S3, DynamoDB, SQS, etc.);executionRoleArnis assumed by the ECS Fargate agent and grants ECR image pull and CloudWatch Logs delivery — both must have a trust policy allowingecs-tasks.amazonaws.com. TheexecutionRoleArnis mandatory on Fargate; on EC2 it is optional (the EC2 instance profile covers ECR + logs via the host ECS agent). GPU jobs (EC2 only) add{type:'GPU', value:'1'}toresourceRequirementsand must specify GPU instance types (p3, g4dn, g5) in the compute environment. Fargate does not supportprivileged,ulimits,devices, host-path volumes, or GPU — if any of these appear in a Fargate job definition,RegisterJobDefinitionCommandreturnsClientException: Fargate does not support Xat registration time, not at submission time. - Array jobs and dependencies — add
arrayProperties:{size:N}(N = 2–10,000) toSubmitJobCommand; Batch creates N child jobs withAWS_BATCH_JOB_ARRAY_INDEX(0 to N-1) injected as an environment variable; child job IDs are{parentId}:0through{parentId}:N-1; pollDescribeJobsCommand({jobs:[parentJobId]})forjob.arrayProperties.statusSummary(a map of state → count); parent reachesSUCCEEDEDonly when ALL children succeed; useListJobsCommand({arrayJobId:parentId, jobStatus:'FAILED'})to find failed children; chain jobs withdependsOn:[{jobId}](no type = wait for all upstream children),dependsOn:[{jobId, type:'SEQUENTIAL'}](child N+1 starts after child N), ordependsOn:[{jobId, type:'N_TO_N'}](index-matched — both arrays must have the same size): Array job manifest pattern: pre-generate a JSON file on S3 listing all input keys and readkeys[parseInt(process.env.AWS_BATCH_JOB_ARRAY_INDEX)]in the container — simpler than encoding the key into an environment variable override per child. MNP (multi-node parallel) jobs for tightly-coupled MPI workloads usetype:'multinode'inRegisterJobDefinitionCommandwithnodeProperties:{numNodes, mainNode, nodeRangeProperties}— Batch injectsAWS_BATCH_JOB_NODE_INDEXandAWS_BATCH_JOB_MAIN_NODE_PRIVATE_IPV4_ADDRESSfor rendezvous. Fair-share: create a policy withCreateSchedulingPolicyCommand({fairsharePolicy:{shares:[{shareIdentifier, weightFactor}]}}), attach to a queue withUpdateJobQueueCommand, then tag each job submission withshareIdentifier— the scheduler gives priority to whichever identifier has consumed less of its weighted share when capacity is constrained.
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:
| type | Underlying fleet | When to use |
|---|---|---|
FARGATE | Fargate on-demand | Simple serverless workloads; no instanceRole required |
FARGATE_SPOT | Fargate Spot | Non-critical Fargate jobs at ~70% savings |
EC2 | On-demand EC2 | GPU workloads, privileged containers, host-path volumes |
SPOT | EC2 Spot Instances | Cost-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
| State | Meaning | Typical duration | MCP action |
|---|---|---|---|
SUBMITTED | Accepted, waiting for scheduler slot | Seconds | Keep polling |
PENDING | Waiting for a dependency job to complete | Until dependency finishes | Keep polling |
RUNNABLE | Ready to run, waiting for capacity | Seconds to minutes | Keep polling; stuck > 5 min → diagnose |
STARTING | Container being pulled and started | Seconds to 2 minutes | Keep polling |
RUNNING | Job container executing | Your workload duration | Keep polling |
SUCCEEDED | Container exited 0 | Terminal | Success |
FAILED | Non-zero exit, timeout, or terminated | Terminal | Failure — 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:
| vCPU | Valid memory values (MiB) |
|---|---|
| 0.25 | 512, 1024, 2048 |
| 0.5 | 1024–4096 (1024 increments) |
| 1 | 2048–8192 (1024 increments) |
| 2 | 4096–16384 (1024 increments) |
| 4 | 8192–30720 (1024 increments) |
| 8 | 16384–61440 (4096 increments) |
| 16 | 32768–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
| Field | Assumed by | Grants access to | Required on |
|---|---|---|---|
jobRoleArn | Your container process | AWS APIs your code calls (S3, DynamoDB, SQS, etc.) | EC2 and Fargate |
executionRoleArn | ECS Fargate agent | ECR image pull, CloudWatch Logs, Secrets Manager, SSM | Fargate 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 Fargate | EC2 alternative |
|---|---|
privileged: true | Use EC2 compute environment |
ulimits | Not configurable on Fargate |
devices | Not configurable on Fargate |
instanceType override | Fargate manages instance selection |
| Host-path volumes | Use 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 type | Semantics | When to use |
|---|---|---|
| (no type) | Downstream starts only when ALL upstream children succeed | Aggregation job after fan-out |
SEQUENTIAL | Child N+1 starts only after child N succeeds (serial) | Serial pipeline where each step builds on previous output |
N_TO_N | Child K of downstream starts when child K of upstream succeeds | Multi-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
- AWS Batch overview — CreateComputeEnvironment, CreateJobQueue, RegisterJobDefinition — full infrastructure setup reference
- AWS Batch job lifecycle — SubmitJob, DescribeJobs, retryStrategy — polling, cancel/terminate, and evaluateOnExit
- AWS Batch container properties — resourceRequirements, Fargate, logConfiguration — full container configuration reference
- AWS Batch array jobs and job dependencies — arrayProperties, dependsOn — fan-out patterns, pipeline dependencies, and fair-share scheduling
- AWS Step Functions for MCP servers — orchestrate Batch jobs in a state machine for complex workflows
- AWS CloudWatch for MCP servers — query Batch job logs and set alarms on job failure rates
- AWS IAM for MCP servers — job role and execution role design patterns
- AWS SQS for MCP servers — alternative fan-out pattern using queues instead of array jobs
- AWS EMR for MCP Servers: Cluster Lifecycle, Serverless Spark, S3A Pattern, and EMR Studio — synthesis of the four EMR patterns for big-data processing workloads