Guide · AWS EMR Serverless · Job Submission
MCP Server EMR Serverless — CreateApplication, StartJobRun, pre-initialized capacity
EMR Serverless lets you run Apache Spark and Apache Hive jobs without provisioning or managing EC2 clusters. You create an application (the billing and configuration boundary) and then submit job runs against it. From an MCP server you call CreateApplicationCommand once, StartApplicationCommand to bring it online, StartJobRunCommand per job, and GetJobRunCommand to poll progress. The key sizing decisions are worker configuration (default 4 vCPU / 16 GB workers; use 8 vCPU / 32 GB for memory-intensive Spark jobs) and pre-initialized capacity (optional warm workers that eliminate the cold-start penalty of ~2 minutes per job).
TL;DR
Create an application with CreateApplicationCommand (type: SPARK or HIVE, releaseLabel). Start it with StartApplicationCommand and wait for STARTED. Submit jobs with StartJobRunCommand (set jobDriver.sparkSubmit.entryPoint and executionRoleArn). Poll GetJobRunCommand until state reaches SUCCESS or FAILED. Configure log delivery in configurationOverrides.monitoringConfiguration.
Application and job run state machines
An EMR Serverless application and each job run have independent state machines. The application must reach STARTED before you can submit job runs.
| Application state | Meaning |
|---|---|
CREATING | Application being created — transient, poll |
CREATED | Created but not started — no workers running, no cost |
STARTING | Workers being initialized (cold start or pre-init spinning up) |
STARTED | Ready to accept job runs |
STOPPING | Workers being released |
STOPPED | No workers running — cannot submit jobs without restarting |
TERMINATED | Application permanently deleted |
| Job run state | Meaning |
|---|---|
SUBMITTED | Queued for execution |
PENDING | Waiting for worker capacity |
SCHEDULED | Workers assigned, driver starting |
RUNNING | Job actively executing |
SUCCESS | Job completed without errors |
FAILED | Job exited with error — check stateDetails |
CANCELLING / CANCELLED | Job was explicitly cancelled via CancelJobRunCommand |
Creating and starting an application
An application is a reusable container for job runs. Create it once per workload type (one for Spark ETL jobs, one for Hive queries, etc.) and reuse it across many job run submissions.
import {
EMRServerlessClient,
CreateApplicationCommand,
StartApplicationCommand,
GetApplicationCommand,
type ApplicationState,
} from '@aws-sdk/client-emr-serverless';
const emrServerless = new EMRServerlessClient({
region: process.env.AWS_REGION ?? 'us-east-1',
});
export async function createSparkApplication(params: {
name: string;
releaseLabel?: string;
}): Promise<string> {
const response = await emrServerless.send(new CreateApplicationCommand({
name: params.name,
type: 'SPARK',
releaseLabel: params.releaseLabel ?? 'emr-7.2.0',
// Worker sizing — override defaults here
workerTypeSpecifications: {
Driver: {
workerConfiguration: {
cpu: '4 vCPU',
memory: '16 GB',
disk: '20 GB',
},
},
Executor: {
workerConfiguration: {
cpu: '4 vCPU',
memory: '16 GB',
disk: '20 GB',
},
},
},
// Auto-stop idle application after 15 minutes (saves cost between job runs)
autoStopConfiguration: {
enabled: true,
idleTimeoutMinutes: 15,
},
// Auto-start when a job run is submitted (no need to call StartApplication explicitly)
autoStartConfiguration: {
enabled: true,
},
tags: { ManagedBy: 'alivemcp' },
}));
return response.applicationId!;
}
export async function startApplication(applicationId: string): Promise<void> {
await emrServerless.send(new StartApplicationCommand({ applicationId }));
// Wait for STARTED state
const deadline = Date.now() + 5 * 60 * 1000;
while (Date.now() < deadline) {
const r = await emrServerless.send(new GetApplicationCommand({ applicationId }));
const state = r.application?.state as ApplicationState;
if (state === 'STARTED') return;
if (state === 'TERMINATED') throw new Error('Application was terminated during start');
await new Promise(res => setTimeout(res, 10_000));
}
throw new Error('Application did not start within 5 minutes');
}
When autoStartConfiguration.enabled is true (the default), submitting a job run on a STOPPED application automatically starts it before queueing the job. This means you do not need to call StartApplicationCommand explicitly for on-demand workloads. For latency-sensitive pipelines where the 60–120 second cold start is unacceptable, use pre-initialized capacity instead.
Submitting a job run with StartJobRunCommand
Job runs reference a Spark or Hive job via the jobDriver field. For Spark jobs, use jobDriver.sparkSubmit with an entryPoint pointing to your Python script or JAR on S3.
import {
StartJobRunCommand,
GetJobRunCommand,
CancelJobRunCommand,
type JobRunState,
} from '@aws-sdk/client-emr-serverless';
export async function submitSparkJob(params: {
applicationId: string;
executionRoleArn: string;
name: string;
entryPoint: string; // s3://bucket/script.py or s3://bucket/app.jar
entryPointArgs?: string[];
sparkSubmitParams?: string; // e.g. '--conf spark.executor.memory=8g --num-executors 4'
logBucket?: string;
executionTimeoutMinutes?: number;
}): Promise<string> {
const response = await emrServerless.send(new StartJobRunCommand({
applicationId: params.applicationId,
name: params.name,
executionRoleArn: params.executionRoleArn,
executionTimeoutMinutes: params.executionTimeoutMinutes ?? 60,
jobDriver: {
sparkSubmit: {
entryPoint: params.entryPoint,
entryPointArguments: params.entryPointArgs ?? [],
sparkSubmitParameters: params.sparkSubmitParams ?? '',
},
},
configurationOverrides: params.logBucket ? {
monitoringConfiguration: {
// Persist Spark logs to S3 for debugging
s3MonitoringConfiguration: {
logUri: `s3://${params.logBucket}/emr-serverless-logs/`,
},
// Also enable the managed log store (available in EMR console)
managedPersistenceMonitoringConfiguration: {
enabled: true,
},
},
} : undefined,
tags: { ManagedBy: 'alivemcp', JobName: params.name },
}));
return response.jobRunId!;
}
export async function waitForJobRun(
applicationId: string,
jobRunId: string,
timeoutMs = 60 * 60 * 1000, // 1h
): Promise<JobRunState> {
const deadline = Date.now() + timeoutMs;
const TERMINAL: JobRunState[] = ['SUCCESS', 'FAILED', 'CANCELLED'];
while (Date.now() < deadline) {
const r = await emrServerless.send(new GetJobRunCommand({
applicationId,
jobRunId,
}));
const jobRun = r.jobRun!;
const state = jobRun.state as JobRunState;
if (TERMINAL.includes(state)) {
if (state !== 'SUCCESS') {
throw new Error(
`Job run ${jobRunId} ${state}: ${jobRun.stateDetails}`
);
}
return state;
}
await new Promise(res => setTimeout(res, 15_000));
}
// Cancel the job before giving up
await emrServerless.send(new CancelJobRunCommand({ applicationId, jobRunId }));
throw new Error(`Job run ${jobRunId} timed out after ${timeoutMs}ms`);
}
Pre-initialized capacity for warm starts
By default, EMR Serverless workers start from scratch when a job run is submitted, taking 60–120 seconds for the first job. Pre-initialized capacity keeps a fixed number of warm workers running 24/7, eliminating this cold start. Use it when job latency matters more than idle cost.
import { CreateApplicationCommand } from '@aws-sdk/client-emr-serverless';
// Application with pre-initialized drivers and executors
export async function createWarmSparkApplication(name: string): Promise<string> {
const response = await emrServerless.send(new CreateApplicationCommand({
name,
type: 'SPARK',
releaseLabel: 'emr-7.2.0',
// Keep 2 driver workers + 10 executor workers warm at all times
initialCapacity: {
Driver: {
workerCount: 2,
workerConfiguration: {
cpu: '4 vCPU',
memory: '16 GB',
disk: '20 GB',
},
},
Executor: {
workerCount: 10,
workerConfiguration: {
cpu: '4 vCPU',
memory: '16 GB',
disk: '20 GB',
},
},
},
// Cap total concurrency to avoid runaway cost
maximumCapacity: {
cpu: '400 vCPU',
memory: '3000 GB',
disk: '20000 GB',
},
// Disable auto-stop so pre-init workers stay warm
autoStopConfiguration: {
enabled: false,
},
autoStartConfiguration: {
enabled: true,
},
}));
return response.applicationId!;
}
| Setup | First job latency | Cost when idle | Best for |
|---|---|---|---|
| No pre-init (default) | 60–120 seconds | $0 (workers only exist during job) | Infrequent batch jobs (<10/day) |
| Pre-init 2 drivers + 10 executors | < 5 seconds | ~$2.60/hr (12 × 4vCPU workers) | Interactive workloads, high-frequency jobs |
Pre-initialized capacity cost: EMR Serverless charges $0.052/vCPU-hour and $0.0057/GB-hour. 10 Executor workers at 4vCPU/16GB idle 24/7 ≈ $1.37/hr ($984/mo). Evaluate against the alternative of running a persistent EC2 cluster for the same workload.
Worker sizing guide
EMR Serverless enforces a fixed memory-to-CPU ratio of 4 GB per vCPU for workers. Valid configurations:
| Worker size | vCPU | Memory | Disk | Use case |
|---|---|---|---|---|
| Small | 1 vCPU | 4 GB | 20 GB | Lightweight scripts, SQL queries |
| Default | 4 vCPU | 16 GB | 20 GB | Standard ETL, general Spark |
| Large | 8 vCPU | 32 GB | 20 GB | Memory-intensive joins, ML feature engineering |
| XLarge | 16 vCPU | 64 GB | 200 GB | Columnar sort, large shuffles, data skew |
For most Spark ETL jobs the default 4 vCPU / 16 GB executor is a reasonable starting point. If you see OOM errors in driver logs (java.lang.OutOfMemoryError: GC overhead limit exceeded), increase the driver memory first (driver collects results). If you see executor OOM during shuffle or broadcast joins, increase executor memory. Disk size (minimum 20 GB, maximum 200 GB) is consumed by shuffle spill — increase it for wide aggregations on large datasets.
VPC connectivity
By default, EMR Serverless workers run in an AWS-managed VPC without access to your VPC. To access RDS, MSK, or VPC-private S3 endpoints from Spark jobs, configure VPC access on the application.
import { CreateApplicationCommand } from '@aws-sdk/client-emr-serverless';
export async function createVpcSparkApplication(params: {
name: string;
subnetIds: string[];
securityGroupIds: string[];
}): Promise<string> {
const response = await emrServerless.send(new CreateApplicationCommand({
name: params.name,
type: 'SPARK',
releaseLabel: 'emr-7.2.0',
networkConfiguration: {
subnetIds: params.subnetIds,
securityGroupIds: params.securityGroupIds,
},
}));
return response.applicationId!;
}
When VPC is configured, all workers attach ENIs in the specified subnets. Ensure the subnets have NAT Gateway access (or VPC endpoints) for S3 — EMR Serverless workers still need to reach S3 to read/write job data, even with VPC networking enabled.
Cost model vs EMR on EC2
| Dimension | EMR Serverless | EMR on EC2 |
|---|---|---|
| Pricing unit | $0.052/vCPU-hr + $0.0057/GB-hr (per worker-second) | EC2 instance price (On-Demand or Spot) + EMR surcharge (~25%) |
| Idle cost | $0 (unless pre-init enabled) | Full instance cost while cluster is WAITING |
| Launch latency | 60–120s cold start (0s with pre-init) | 5–10 min cluster bootstrap |
| Max cluster size | Governed by maximumCapacity | Governed by EC2 vCPU quota + EMR quota |
| Best for | Infrequent jobs, bursty workloads | Continuous streaming, interactive queries needing predictable capacity |
For a typical ETL job running 30 minutes using 4 drivers + 20 executors at 4vCPU/16GB each: EMR Serverless ≈ 24 workers × 4vCPU × 0.5h × $0.052 + 24 × 16GB × 0.5h × $0.0057 ≈ $2.50 per job run. Compare against running an m5.xlarge (4vCPU/16GB) EC2 cluster of equivalent size for 30 minutes at ~$0.192/hr × 20 × 0.5h ≈ $1.92 On-Demand or ~$0.60 Spot. For high-frequency jobs, EC2 Spot clusters will be cheaper; for infrequent bursts, EMR Serverless avoids idle charges.
Related pages
- EMR cluster management — RunJobFlow, AddJobFlowSteps, termination protection — EC2-backed EMR clusters
- EMR Spark job patterns — spark-submit, S3A, Glue metastore, dynamic allocation — Spark-specific configuration
- EMR Studio — CreateStudio, CreateEditor, SparkMagic notebooks — interactive development environment
- AWS Glue ETL jobs for MCP servers
- AWS Lake Formation for MCP servers