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:

ConfigurationTask slots per KPUParallelism for 4 KPUsWhen to use
Default (parallelism-per-kpu = 1)14CPU-bound workloads; window aggregation with heavy computation per record
parallelism-per-kpu = 228I/O-bound workloads; records spend time waiting for external calls (JDBC enrichment)
parallelism-per-kpu = 4416Lightweight 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:

MetricScale-out thresholdScale-in thresholdCooldown period
cpuUtilization> 80% sustained< 25% sustainedScale-out: 3 min; Scale-in: 15 min
heapMemoryUtilization> 75% sustained< 40% sustainedSame as CPU
millisBehindLatest (KDS source lag)Growing continuouslyNear 0Incorporated 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:

ScaleProbe events/minInput throughputRecommended KPUsKinesis shardsMonthly KPU cost
5,000 endpoints5,000/min = 83/s~42 KB/s1 KPU (single shard)1 shard~$53/month (1 KPU × 720h × $0.080/KPU-hr)
20,000 endpoints20,000/min = 333/s~167 KB/s2 KPUs2 shards (1 MB/s each)~$115/month
50,000 endpoints50,000/min = 833/s~417 KB/s4 KPUs4 shards~$230/month
200,000 endpoints200,000/min = 3,333/s~1.7 MB/s8 KPUs8 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

SymptomRoot causeDetectionFix
millisBehindLatest growing continuously despite low CPUSource parallelism lower than shard count; Flink single-task reading all shards sequentiallyCloudWatch 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-inPredictable hourly burst causing scale-out; 15-min cooldown expires and scale-in triggers before next burstuptime CloudWatch metric resetting every ~30-40 min; application restart logs at regular intervalsSet minimum KPU to cover burst load; disable autoscaling and use fixed KPU count for predictable workloads
KPU count 10× higher than expectedparallelism-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 ExplorerCheck that ParallelismPerKPU = 1 in application configuration; ensure single Flink job per Managed Flink application
Window results disappear after scale eventAutoscaling triggered during window accumulation; checkpoint taken mid-window, in-window state lost if checkpoint was incompleteMissing windows in output stream for the period spanning the scale event; Flink logs show restore from checkpoint with partial window stateEnsure checkpoint interval ≤ window size; verify exactly-once mode; check that window state is correctly included in checkpoint
Scale-out takes 15+ minutesLarge RocksDB state requiring full snapshot and restore on scale eventApplication in STARTING state for >5 minutes after scale event; lastCheckpointSize > 5 GBEnable incremental checkpoints; use manual savepoint-based scaling to schedule during low-traffic periods; consider state expiry with StateTtlConfig to reduce state size