Guide · Managed Flink Parallelism
Managed Flink Parallelism and KPU Scaling for MCP Servers — Task Slots, Autoscaling, Capacity Planning
A Kinesis Processing Unit (KPU) is Managed Flink's unit of compute: 1 KPU = 1 vCPU + 4 GB RAM. By default, each KPU provides one task slot — the unit of work Flink uses to execute a parallel pipeline instance. Setting application parallelism to N means Flink tries to assign N task slots, which requires at least N KPUs. The cost trap is that Managed Flink bills KPU-hours regardless of whether the application is actively processing records — an idle 4-KPU application costs the same as a busy one. The scaling question for MCP probe analytics pipelines is not just "how many KPUs do I need at peak?" but "what is the minimum KPU count that doesn't fall behind during the once-per-hour spike when all registered endpoints are re-probed simultaneously?" That determines the floor for autoscaling, and getting it wrong means your window aggregations fall behind, watermarks stall, and late-data alarms trigger.
TL;DR
Set application parallelism to match the number of Kinesis shards on your source stream (one parallel task per shard is the natural scaling point for the KDS source). Each parallel task requires one KPU. For autoscaling, set autoscalingEnabled: true with a minimum of 1 KPU and a maximum appropriate for your peak throughput. Autoscaling takes 1–3 minutes and restarts the application from the last checkpoint — ensure your checkpoint interval is ≤60 seconds so restart latency is bounded. For large-state applications (RocksDB with GB-scale state), disable autoscaling and scale manually via savepoints to avoid the state restoration overhead of frequent autoscaling events.
KPU, parallelism, and task slots — the relationship
Understanding the KPU-to-parallelism mapping is prerequisite to capacity planning. The default Managed Flink configuration allocates one task slot per KPU, so application parallelism equals KPU count in the default setup. But task slot allocation can be tuned:
| Configuration | Task slots per KPU | Parallelism for 4 KPUs | When to use |
|---|---|---|---|
| Default (parallelism-per-kpu = 1) | 1 | 4 | CPU-bound workloads; window aggregation with heavy computation per record |
| parallelism-per-kpu = 2 | 2 | 8 | I/O-bound workloads; records spend time waiting for external calls (JDBC enrichment) |
| parallelism-per-kpu = 4 | 4 | 16 | Lightweight transformation pipelines with very low CPU per record |
For AliveMCP probe analytics pipelines, the workload is CPU-bound at the window aggregation step (JSON deserialization + UDF for uptime percentage calculation) and I/O-bound at the source step (reading from Kinesis). The default parallelism-per-kpu = 1 is correct — increasing it risks CPU contention on the aggregation tasks without proportional throughput gain.
// Application-level parallelism — set in Managed Flink application configuration
// This is the top-level parallelism that Flink distributes across available task slots
{
"ParallelismConfiguration": {
"ConfigurationType": "CUSTOM",
"Parallelism": 4, // Number of parallel task instances
"ParallelismPerKPU": 1, // Task slots per KPU (default)
"AutoScalingEnabled": true // Allow Managed Flink to adjust Parallelism automatically
}
}
When ConfigurationType is set to DEFAULT, Managed Flink ignores Parallelism and ParallelismPerKPU and uses defaults (1 KPU, parallelism = 1). Use CUSTOM to control the KPU count and task slot allocation explicitly.
Operator-level parallelism overrides
The application-level parallelism sets the default parallelism for all operators. Individual operators can override this with setParallelism(n). Common reasons to override:
// Pattern: Kinesis source parallelism = shard count
// The KDS source cannot benefit from parallelism higher than the shard count —
// each parallel KDS consumer reads one or more shards, but a shard can only be
// read by one consumer at a time. Extra parallel tasks sit idle.
DataStream<String> source = env
.addSource(new FlinkKinesisConsumer<>(...))
.uid("kinesis-source")
.setParallelism(4); // Set to match shard count (4 shards in this example)
// Pattern: Aggregation operator at full application parallelism
// Window aggregation benefits from the full parallelism setting for
// key-distributed aggregation (Flink hashes serverSlug to distribute
// keys evenly across parallel tasks)
DataStream<ServerUptimeWindow> windows = source
.keyBy(ProbeResult::getServerSlug)
.window(TumblingEventTimeWindows.of(Time.minutes(15)))
.aggregate(new UptimeAggregator())
.uid("uptime-aggregator");
// No .setParallelism() — inherits application-level parallelism (4)
// Pattern: Sink at reduced parallelism to avoid Firehose throttling
// PutRecordBatch limit: 500 records/batch, 4 MB/batch, 5,000 records/s per stream
// For low-throughput output streams, reduce sink parallelism to avoid
// creating too many Firehose producers competing for the same stream throughput limit
DataStream<String> output = windows
.map(new UptimeWindowSerializer())
.addSink(new KinesisFirehoseSink<>(...))
.uid("firehose-sink")
.setParallelism(1); // Single sink task for Firehose output (low throughput)
A common mistake is setting source parallelism higher than the Kinesis shard count. Flink assigns shards to parallel tasks round-robin — with 4 shards and 8 parallel source tasks, 4 tasks read one shard each and 4 tasks are idle. The idle tasks consume KPU-hours without contributing throughput. Match source parallelism to shard count, then scale shards (via Kinesis resharding) when throughput grows.
Autoscaling behavior and triggers
When AutoScalingEnabled: true, Managed Flink monitors application metrics and adjusts the KPU count (and parallelism) automatically. The scaling logic uses two primary signals:
| Metric | Scale-out threshold | Scale-in threshold | Cooldown period |
|---|---|---|---|
| cpuUtilization | > 80% sustained | < 25% sustained | Scale-out: 3 min; Scale-in: 15 min |
| heapMemoryUtilization | > 75% sustained | < 40% sustained | Same as CPU |
| millisBehindLatest (KDS source lag) | Growing continuously | Near 0 | Incorporated into scale-out decision |
Autoscaling triggers an application restart: Managed Flink takes a checkpoint (or uses the latest checkpoint), updates the KPU count and parallelism, then restarts the application from the checkpoint. The restart takes 60–180 seconds. During restart, records continue to accumulate on the Kinesis source stream (they are not lost — Kinesis retains data for the configured retention period). After restart, the application catches up by processing the buffered records.
For AliveMCP probe analytics, autoscaling scale-out events typically occur at the start of each hour (when registry re-scans produce a burst of probe results) and scale back in 15 minutes later. This is a predictable pattern that can be handled by setting the minimum KPU count high enough to handle the hourly burst without triggering autoscaling at all — trading KPU-hour cost for stability.
Scaling with state: savepoint vs in-place autoscaling
Flink's autoscaling in Managed Flink is not in-place. It restarts the application, which means state must be checkpointed before the scale event and restored after. For large-state applications (RocksDB with multi-GB state), this restart can take 5–20 minutes for state restoration, during which the source stream accumulates backlog.
Two approaches to manage scaling for stateful applications:
// Approach 1: Autoscaling (recommended for small-state applications)
// - Let Managed Flink autoscale automatically
// - Application restarts from last checkpoint on each scale event
// - Acceptable when checkpoint size is <500 MB and restore takes <2 minutes
{
"ParallelismConfiguration": {
"AutoScalingEnabled": true,
"Parallelism": 2, // Starting value; Managed Flink adjusts this
"ParallelismPerKPU": 1
},
"MonitoringConfiguration": {
"MetricsLevel": "TASK" // Required to emit per-operator metrics for autoscaling decisions
}
}
// Approach 2: Manual scaling via savepoint (for large-state or latency-sensitive applications)
// Step 1: Take a savepoint via Managed Flink API
aws kinesisanalyticsv2 create-application-snapshot \
--application-name alivemcp-probe-analytics \
--snapshot-name "pre-scale-$(date +%Y%m%d%H%M)"
// Step 2: Update application configuration with new KPU count
aws kinesisanalyticsv2 update-application \
--application-name alivemcp-probe-analytics \
--current-application-version-id $version \
--application-configuration-update '{
"FlinkApplicationConfigurationUpdate": {
"ParallelismConfigurationUpdate": {
"AutoScalingEnabledUpdate": false,
"ParallelismUpdate": 8,
"ParallelismPerKPUUpdate": 1
}
}
}'
// Step 3: Start the application from the savepoint
aws kinesisanalyticsv2 start-application \
--application-name alivemcp-probe-analytics \
--run-configuration '{
"ApplicationRestoreConfiguration": {
"ApplicationRestoreType": "RESTORE_FROM_CUSTOM_SNAPSHOT",
"SnapshotName": "pre-scale-20261030"
}
}'
Manual scaling via savepoints gives you control over the timing (scale during low-traffic windows), the ability to verify the savepoint before committing to the scale event, and a rollback path (the savepoint is preserved until explicitly deleted).
Capacity planning for MCP probe analytics workloads
AliveMCP probes MCP endpoints every 60 seconds. Each probe result is a JSON record of ~500 bytes (endpoint slug, status code, response time, error code, timestamp). At different scale points:
| Scale | Probe events/min | Input throughput | Recommended KPUs | Kinesis shards | Monthly KPU cost |
|---|---|---|---|---|---|
| 5,000 endpoints | 5,000/min = 83/s | ~42 KB/s | 1 KPU (single shard) | 1 shard | ~$53/month (1 KPU × 720h × $0.080/KPU-hr) |
| 20,000 endpoints | 20,000/min = 333/s | ~167 KB/s | 2 KPUs | 2 shards (1 MB/s each) | ~$115/month |
| 50,000 endpoints | 50,000/min = 833/s | ~417 KB/s | 4 KPUs | 4 shards | ~$230/month |
| 200,000 endpoints | 200,000/min = 3,333/s | ~1.7 MB/s | 8 KPUs | 8 shards | ~$461/month |
The throughput numbers above are for ingestion only. If the analytics pipeline includes enrichment (joining probe results with a reference dataset of server metadata from S3 or RDS), add 20–50% to the KPU count to cover the enrichment CPU cost. If the pipeline aggregates results and fans out to multiple sinks (Kinesis output + Firehose archive + OpenSearch), add one KPU per 2–3 additional sink operators.
The most common capacity mistake for MCP probe analytics is sizing for average throughput and ignoring the registry-scan spike. When AliveMCP re-scans the full MCP registry (every 6 hours), ~5,000 new endpoint records arrive in the Kinesis stream within 2–3 minutes. This is a 60× burst above average. Size the Kinesis stream for the burst rate, not the average, and configure Kinesis enhanced fan-out for the Flink consumer to ensure dedicated 2 MB/s throughput per shard during the burst.
Complete failure mode reference
| Symptom | Root cause | Detection | Fix |
|---|---|---|---|
| millisBehindLatest growing continuously despite low CPU | Source parallelism lower than shard count; Flink single-task reading all shards sequentially | CloudWatch millisBehindLatest > 60,000 ms; cpuUtilization < 50% (source is the bottleneck, not CPU) | Increase source parallelism to match shard count; increase KPU count to match |
| Autoscaling repeatedly triggering scale-out then scale-in | Predictable hourly burst causing scale-out; 15-min cooldown expires and scale-in triggers before next burst | uptime CloudWatch metric resetting every ~30-40 min; application restart logs at regular intervals | Set minimum KPU to cover burst load; disable autoscaling and use fixed KPU count for predictable workloads |
| KPU count 10× higher than expected | parallelism-per-kpu set to fractional value or application running multiple Flink jobs (multi-job JAR) | Managed Flink console shows unexpectedly high KPU count; billing anomaly in AWS Cost Explorer | Check that ParallelismPerKPU = 1 in application configuration; ensure single Flink job per Managed Flink application |
| Window results disappear after scale event | Autoscaling triggered during window accumulation; checkpoint taken mid-window, in-window state lost if checkpoint was incomplete | Missing windows in output stream for the period spanning the scale event; Flink logs show restore from checkpoint with partial window state | Ensure checkpoint interval ≤ window size; verify exactly-once mode; check that window state is correctly included in checkpoint |
| Scale-out takes 15+ minutes | Large RocksDB state requiring full snapshot and restore on scale event | Application in STARTING state for >5 minutes after scale event; lastCheckpointSize > 5 GB | Enable incremental checkpoints; use manual savepoint-based scaling to schedule during low-traffic periods; consider state expiry with StateTtlConfig to reduce state size |