AWS MSK · 2026-10-30 · MSK Advanced arc

MSK Advanced Patterns for MCP Server Monitoring Pipelines: Serverless vs Provisioned, Durability Model, Schema Evolution, and Producer Tuning

MSK becomes operationally reliable for MCP monitoring pipelines only when four interconnected decisions are made correctly — and the failure modes of each are invisible until they surface as silent data loss, duplicate probe results, or a 2 AM deployment that breaks schema-sharing consumers. MSK Serverless removes partition pre-provisioning and scales ingress automatically up to 200 MiBps — but it enforces IAM-only authentication, excludes MSK Connect, MSK Replication, and log compaction, and caps partitions at a 120-partition soft limit; choosing Serverless for a probe pipeline that needs per-topic retention overrides or must replicate to a standby region is a commitment that requires re-platforming to provisioned MSK later, not a config change. Kafka's durability model is a three-way system: replication factor (how many broker copies), min.insync.replicas (how many must be caught up), and producer acks (how many must acknowledge a write) — the standard durable probe result configuration is RF=3/min.ISR=2/acks=all, which means a write is acknowledged only after two of the three broker copies confirm it, tolerating one broker failure without data loss; combining this with enable.idempotence=true prevents the producer from writing a duplicate result when a network timeout causes a retry after a successful write that lost its acknowledgement; and bounding retries with delivery.timeout.ms=30000 aligns the retry window with the 60-second probe cycle so a failed write expires cleanly rather than retrying into a future probe cycle. Glue Schema Registry enforces schema contracts at send time rather than at consumer deploy time — a producer code change that renames a required field compiles correctly and only fails when the Glue serializer tries to register the new schema version and the compatibility check rejects it; the BACKWARD compatibility mode (the right choice for most probe payload evolution) allows adding new optional fields with defaults and removing existing optional fields, but rejects field renames, type changes, and removing required fields — catching schema breakage at CI/CD time instead of at runtime when the archiver Lambda silently starts dropping deserialization errors; cross-account consumers (an analytics pipeline in a separate AWS account reading probe events) need only a resource-based policy on the Glue registry granting glue:GetSchemaVersion and glue:GetSchemaByDefinition — no VPC peering required because Glue Schema Registry is a regional API endpoint. Producer throughput tuning turns 2,000 individual probe result sends per second into 60 batched Kafka requests per second — the default batch.size=16384 with linger.ms=0 sends each probe result as a separate Kafka request, incurring the full SASL/TLS round-trip overhead per result; setting batch.size=65536 (64 KB) and linger.ms=5 accumulates up to 64 KB of probe results or 5ms of messages — whichever comes first — and sends them as one compressed Kafka request; at 2,000 probe results/second with average probe result size of 1.5 KB and LZ4 compression achieving 4×, each 64 KB batch holds roughly 170 uncompressed results (16 KB compressed), which means the batch fills in under 100ms and the 5ms linger is rarely the trigger; the result is roughly 33 Kafka requests per second instead of 2,000 — a 60× reduction in broker connection overhead and a meaningful cut in MSK data-in charges. This guide synthesises all four patterns. Pattern 1 covers MSK Serverless — the capacity model, IAM-only auth constraints, feature exclusions vs provisioned, CDK setup, and when provisioned MSK is the right choice — synthesised from MSK Serverless for MCP Servers. Pattern 2 covers the Kafka durability model — replication factor, min.insync.replicas, producer acks, idempotent producer, delivery.timeout.ms, and MSK Replication for cross-region DR — synthesised from MSK Replication for MCP Servers. Pattern 3 covers Glue Schema Registry — the 5-byte message header, compatibility modes, probe payload schema evolution, CI/CD enforcement, and cross-account access — synthesised from MSK Schema Registry for MCP Servers. Pattern 4 covers producer configuration — acks by topic type, LZ4 compression, batch.size + linger.ms throughput math, idempotent producer constraints, and SASL_SSL config — synthesised from MSK Producer Configuration for MCP Servers.

TL;DR

Pattern 1 — MSK Serverless: Capacity Model, IAM Auth, and Feature Trade-offs

Why MSK Serverless changes the capacity planning conversation

On a provisioned MSK cluster, throughput is a product of broker count, instance type, and partition count: you plan in advance, size conservatively to handle peak probe waves, and pay for that capacity whether you use it or not. MSK Serverless replaces that model with a single per-cluster soft quota:

MetricMSK ServerlessProvisioned MSK (comparison)
Ingress throughput200 MiBps per clusterScales with broker count and instance type
Egress throughput400 MiBps per clusterScales with broker count and instance type
Partitions per cluster120 (soft limit)Up to 300,000 (depends on instance)
Per-topic retention overridesNot supportedSupported via retention.ms topic config
Log compactionNot supportedSupported via cleanup.policy=compact
MSK ConnectNot availableFully supported
MSK Replication (cross-region)Not availableFully supported
Pricing modelPer-GiB-hour + per-MB transferredPer-broker-hour + per-GB storage

For AliveMCP probe pipelines, MSK Serverless is a good fit when probe ingest volume is bursty — many probes completing simultaneously during a scan wave — and when consumer throughput is predictable and bounded by the 120-partition limit. At 2,000 probe completions per second producing roughly 1.5 KB each, peak ingress is approximately 3 MiBps, well within the 200 MiBps quota. The binding constraint for scale-out is the 120-partition soft limit: each consumer thread needs its own partition for ordered processing, so a probe fleet with more than ~120 concurrent consumer threads requires provisioned MSK.

IAM-only authentication: what changes at the client level

MSK Serverless enforces SASL/IAM for every client connection. There is no path to unauthenticated, SCRAM-SHA-512, or mTLS access — the cluster rejects connections that do not present a valid IAM credential. Every producer and consumer must:

  1. Have an IAM principal (Lambda execution role, ECS task role, EC2 instance profile, or explicit access key)
  2. Use the SASL_SSL protocol with sasl.mechanism=AWS_MSK_IAM
  3. Include the aws-msk-iam-sasl-signer-js (Node.js), aws-msk-iam-auth (Java), or equivalent library that signs tokens using SigV4
  4. Hold an IAM policy granting at minimum: kafka-cluster:Connect, kafka-cluster:DescribeTopic, and kafka-cluster:WriteData (for producers) or kafka-cluster:ReadData + kafka-cluster:DescribeGroup + kafka-cluster:AlterGroup (for consumers)

MSK IAM tokens expire after 15 minutes; the client library handles token refresh automatically. If the IAM role is missing kafka-cluster:AlterGroup, the consumer can read messages but cannot commit offsets — commits fail silently in some client versions, causing the consumer to re-read from the last committed offset after every restart. Always verify the full permission set in staging before moving to production.

When to choose provisioned MSK instead

MSK Serverless is the wrong choice for probe pipelines that need any of the following. If you need even one of these, start with provisioned MSK:

// CDK: MSK Serverless cluster with IAM auth
import { aws_msk as msk, aws_iam as iam } from 'aws-cdk-lib';

const serverlessCluster = new msk.CfnServerlessCluster(this, 'ProbeCluster', {
  clusterName: 'alivemcp-probe-serverless',
  clientAuthentication: {
    sasl: { iam: { enabled: true } }
  },
  vpcConfigs: [{
    subnetIds: vpc.privateSubnets.map(s => s.subnetId),
    securityGroups: [probeClusterSg.securityGroupId],
  }],
});

// Grant producer access to probe worker role
const producerPolicy = new iam.PolicyStatement({
  actions: [
    'kafka-cluster:Connect',
    'kafka-cluster:DescribeTopic',
    'kafka-cluster:WriteData',
    'kafka-cluster:CreateTopic',
  ],
  resources: [
    serverlessCluster.attrArn,
    `${serverlessCluster.attrArn}/topic/probe-results`,
    `${serverlessCluster.attrArn}/topic/probe-commands`,
  ],
});
probeWorkerRole.addToPolicy(producerPolicy);

Pattern 2 — Kafka Durability Model: RF, min.ISR, acks, and Idempotence

The three-way durability system

Kafka durability is not controlled by a single setting. It is the product of three independent configurations that must be set consistently to achieve the intended guarantee. Getting one wrong without the others renders the guarantee weaker than intended:

  1. Replication factor (RF) — how many broker copies of each partition exist. RF=1 means there is only one copy; if that broker fails before the message is consumed, the data is gone. RF=3 means three brokers each hold a copy; the cluster can survive one broker failure without losing data.
  2. min.insync.replicas (min.ISR) — the minimum number of in-sync replicas (replicas caught up within the lag threshold) that must acknowledge a write with acks=all before the producer considers it durable. With RF=3 and min.ISR=2: the write is acknowledged when two of three brokers confirm it; one broker can fail after acknowledgement without data loss.
  3. Producer acks — which acknowledgement the broker sends before the producer's send() call returns. acks=0: no ack (fire and forget); acks=1: leader only; acks=all: all ISR members (subject to min.ISR).
ConfigurationDurability guaranteeAvailability trade-offRecommended for
RF=1, acks=1Data lost if broker fails before consumeHighest availabilityDevelopment/staging only
RF=3, min.ISR=1, acks=1Data lost if leader fails before replicationGood: partition survives with 1 brokerProbe command topics (idempotent, can replay)
RF=3, min.ISR=2, acks=allLost only if 2 of 3 brokers fail simultaneouslyUnavailable if fewer than 2 brokers aliveProbe result topics (must not drop)
RF=3, min.ISR=3, acks=allMaximum durability: all 3 must ackUnavailable on any single broker failureAudit/compliance topics only

The standard AliveMCP configuration for the probe-results topic is RF=3/min.ISR=2/acks=all. This means: a probe result is considered written when two of the three MSK brokers have confirmed it. If one broker fails after acknowledgement, the two remaining copies are sufficient. If a second broker fails before the first recovers, the topic becomes unavailable for acks=all producers — they receive NotEnoughReplicasException and must retry. This is the right trade-off: briefly unavailable is better than silently losing monitoring data.

Idempotent producer: preventing duplicates on retry

With acks=all and retries enabled, there is a subtle failure mode: the producer sends a probe result, all in-sync replicas write it, the leader sends the acknowledgement, but the network drops the ack before it reaches the producer. The producer times out and retries. The broker receives the retry and writes it again — now there are two copies of the same probe result. Downstream consumers process it twice: duplicate alert, double-counted latency sample, two rows in the verdict store with slightly different processing timestamps.

Setting enable.idempotence=true solves this. The broker assigns each producer a session-scoped producer ID (PID) and sequences every message per-partition. When a retry arrives with the same PID and sequence number as an already-written message, the broker deduplicates it silently — the retry succeeds (returns success to the producer) but only one copy is stored. Key constraints:

Bounding retries with delivery.timeout.ms

With retries=Integer.MAX_VALUE, a producer that hits a NotEnoughReplicasException will retry indefinitely. For a 60-second probe cycle, retrying indefinitely means a probe result from one cycle can still be in-flight when the next cycle begins — creating a time-ordering problem in the verdict store. Setting delivery.timeout.ms=30000 bounds the total retry window to 30 seconds: after 30 seconds, the send fails with a TimeoutException and the probe result is dropped. The next probe cycle will re-verify the endpoint status — a 30-second delivery window aligns probe result delivery with the probe cycle rather than leaking across cycle boundaries.

// CDK: MSK topic configuration for durable probe result ingestion
import { aws_msk as msk } from 'aws-cdk-lib';

const probeCluster = new msk.Cluster(this, 'ProbeCluster', {
  clusterName: 'alivemcp-probe',
  kafkaVersion: msk.KafkaVersion.V3_5_1,
  numberOfBrokerNodes: 3,
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE),
  encryptionInTransit: {
    clientBroker: msk.ClientBrokerEncryption.TLS,
  },
});

// Producer config for probe-results topic (Node.js kafkajs)
const producer = kafka.producer({
  acks: -1,                          // acks=all
  idempotent: true,                  // enable.idempotence=true
  maxInFlightRequests: 5,            // enforced by idempotence
  transactionTimeout: 30000,
});

// delivery.timeout.ms equivalent: message-level timeout
const result = await producer.send({
  topic: 'probe-results',
  timeout: 30000,                    // delivery.timeout.ms=30000
  messages: [{ key: endpointId, value: JSON.stringify(probeResult) }],
});

MSK Replication for cross-region DR (provisioned only)

MSK Replication (generally available since 2024) copies topics from a source provisioned MSK cluster to a destination cluster in a different AWS region. Unlike MirrorMaker2, MSK Replication is fully managed: no connector to run, no consumer group to monitor. On failover, restart the standby consumer group from the last committed offset on the destination cluster — synchroniseConsumerGroupOffsets translates source offsets to destination offsets during replication, eliminating the need for manual offset reset. Monitor ReplicationByteLag in CloudWatch: a steady non-zero lag means the destination is catching up; a continuously increasing lag means replication is falling behind and the failover RTO will include a replay period. Alert at >50 MB lag for probe pipelines where freshness matters.

Pattern 3 — Glue Schema Registry: Compatibility Modes, Probe Payload Versioning, and Cross-Account Access

How Glue Schema Registry integrates with MSK messages

When a Glue-aware Kafka producer sends a message, the serializer prepends a 5-byte header to the message value: 1 magic byte (0x03 — the Glue magic byte, not the Confluent 0x00) followed by a 4-byte schema-version UUID index. The consumer deserializer reads the header, calls glue:GetSchemaVersion to retrieve the schema definition, and uses it to decode the payload. Schemas are cached per producer/consumer session — the Glue API is not called per message. This architecture means a producer code change that makes an incompatible schema change fails at schema registration time (when the producer starts and tries to register its schema version), not at runtime when a consumer fails to deserialize.

The Glue Schema Registry uses a different API surface than the Confluent Schema Registry. There is no schema.registry.url configuration property. Instead, producers and consumers are configured with AWS credentials and a Glue registry name; the Glue SDK resolves schema versions via the regional Glue API endpoint. This is important for library selection: Confluent's KafkaAvroSerializer does not work with Glue's registry — you need the AWS Glue Schema Registry SerDe libraries.

Compatibility modes: what each allows and rejects

ModeNew schema can…New schema cannot…Best for…
BACKWARDAdd optional fields with defaults; remove optional fieldsRemove required fields; change field types; rename fieldsConsumers upgrade before producers (old consumer reads new messages)
FORWARDRemove optional fields; add fields without defaultsAdd required fields; change field typesProducers upgrade before consumers (new consumer reads old messages)
FULLAdd or remove optional fields with defaults onlyRemove required fields; change types; renameRolling deployments: producer and consumer may run any version simultaneously
NONEAnythingNothing (no compatibility check)Development only — never production
BACKWARD_ALLCompatible with all previous versions (not just latest)Same as BACKWARDLong-tail consumers on older versions
FORWARD_ALLCompatible with all previous versionsSame as FORWARDLong-tail producers on older versions

For AliveMCP probe result schemas, BACKWARD is the correct mode. The probe worker fleet is updated before the archiver and alert Lambda fleet — producers upgrade first, consumers catch up. BACKWARD allows adding new optional fields (for example, tlsCertExpiresAt with a null default when certificate expiry monitoring is added) while ensuring that old consumer versions — still running during a rolling deployment — can still deserialize the new message format because the new field has a default.

Schema evolution examples for probe payloads

The evolution rules are concrete once mapped to probe payload fields:

ChangeBACKWARD-compatible?Why
Add tlsCertExpiresAt (optional, default null)YesOld consumers ignore unknown fields with defaults; new field does not break deserialization
Add schemaHash (required, no default)NoOld consumers receive a message with schemaHash but their schema version has no definition for it — deserialization fails
Rename responseTimeMs to latencyMsNoThe field name change is a rename — old consumers look for responseTimeMs and get null; new consumers look for latencyMs and old messages have neither
Change status from string to enumNoType change breaks deserialization across schema versions
Remove debugPayload (optional field)YesOptional fields can be removed under BACKWARD; consumers that expect the field get the schema-defined default

The Glue serializer validates the new schema version against the compatibility mode at registration time. A BACKWARD violation causes the RegisterSchemaVersion API call to fail with a compatibility error — the producer fails to start, not at message send time. This makes schema violations a CI/CD-time failure rather than a production incident: run the producer startup in a staging environment with schema registration enabled, and any BACKWARD violation fails the deployment pipeline before reaching production consumers.

Cross-account consumer access

For analytics pipelines in a separate AWS account that need to read and deserialize probe events, the Glue Schema Registry access model does not require VPC peering or PrivateLink. The Glue API is a regional public endpoint. The pattern is a resource-based policy on the registry:

// Resource-based policy on Glue registry granting cross-account read access
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ANALYTICS-ACCOUNT-ID:root"
      },
      "Action": [
        "glue:GetSchemaVersion",
        "glue:GetSchemaByDefinition",
        "glue:QuerySchemaVersionMetadata",
        "glue:GetRegistry"
      ],
      "Resource": "arn:aws:glue:us-east-1:PROBE-ACCOUNT-ID:registry/alivemcp-probe-schemas"
    }
  ]
}

With this policy, the analytics account consumer calls glue:GetSchemaVersion using its own IAM credentials — no need for the producer account to assume roles or pre-share schema definitions. Schema lookups are cached in the consumer process for the session lifetime, so the cross-account API call is not on the hot path for message deserialization.

Pattern 4 — Producer Configuration: acks, Compression, Batching, and Idempotence

Two topic types, two producer profiles

A probe pipeline typically has two distinct Kafka topics with different requirements:

Running the same producer configuration for both topics optimizes for neither. Configure separate Kafka.producer() instances with different settings, or use per-topic overrides where supported by your client library.

LZ4 compression: the right choice for probe JSON

Probe result messages are structured JSON with predictable field names (endpointId, status, responseTimeMs, probedAt, etc.) — highly compressible because the field names repeat identically across millions of messages. Kafka compresses at batch level — all messages in a batch are compressed together, which achieves better compression than per-message compression because repeated field names across messages become a single dictionary entry.

CodecCompression ratio on probe JSONCPU costRecommendation
none1× (uncompressed)NoneProbe-commands only (small, latency-sensitive)
gzip5–8×High — CPU boundAvoid on high-volume producers
snappy3–4×Low-mediumGood general-purpose alternative to LZ4
lz43–5×Low — best CPU/ratio trade-offBest for high-throughput probe-results ingest
zstd5–9×MediumBest ratio/CPU if broker storage is the binding constraint

LZ4 is the preferred codec for the probe-results topic. It achieves 3–5× compression at very low CPU overhead, keeping Lambda and ECS probe worker CPU free for the actual HTTP probe requests rather than message compression. The broker stores messages in their compressed form; consumers pay the decompression cost on fetch, but LZ4 decompression is also fast. Set compression.type=producer on the broker (the default) to respect the producer's codec choice rather than recompressing.

batch.size and linger.ms: the batching math

The producer accumulates messages into a batch before flushing to the broker. The batch is flushed when either the batch reaches batch.size bytes or linger.ms elapses, whichever comes first.

Default settings: batch.size=16384 (16 KB), linger.ms=0. With linger.ms=0, the producer sends every message the instant it arrives — the batch fills only if the producer is faster than the network layer picks it up. For a probe worker producing one message per probe result, this means one Kafka request per probe result — 2,000 results/second = 2,000 Kafka requests/second, each incurring a SASL/TLS round-trip overhead.

Tuned settings: batch.size=65536 (64 KB), linger.ms=5. At 2,000 probe results/second with average probe result size of 1.5 KB (uncompressed) and LZ4 compression achieving 4× compression, each message compresses to roughly 375 bytes. A 64 KB batch holds roughly 170 compressed messages. At 2,000 messages/second, the batch fills in about 85ms — the batch.size trigger fires before linger.ms in most cases. The result is roughly 24 Kafka requests per second — an 83× reduction in broker connection overhead. Even without LZ4, each 1.5 KB uncompressed message means the 64 KB batch fills in ~32ms at 2,000 messages/second, yielding ~62 requests/second instead of 2,000.

Profilebatch.sizelinger.msApprox requests/s at 2000 msg/sUse case
Default (low-latency)16 KB0ms~2000 (1:1)probe-commands (latency-sensitive)
Balanced32 KB5ms~100–200medium-volume topics
High-throughput64 KB5ms~24–62 (with LZ4)probe-results (high volume, durable)

Idempotence: enforcement and constraints

Setting enable.idempotence=true on the producer activates the broker-side deduplication mechanism described in Pattern 2. At the producer configuration level, idempotence automatically enforces three constraints — setting conflicting values causes the Kafka client to throw a configuration error at startup:

For the Node.js kafkajs library, idempotence is enabled via idempotent: true in the producer config, which maps to enable.idempotence=true. The library will enforce maxInFlightRequests: 5 automatically. Do not set maxInFlightRequests above 5 in the same config or the producer will fail to initialize.

Combined CDK configuration

// Full producer config for probe-results and probe-commands topics
import { Kafka } from 'kafkajs';
import { MSKAuthProvider } from 'aws-msk-iam-sasl-signer-js';

const kafka = new Kafka({
  clientId: 'alivemcp-probe-worker',
  brokers: [process.env.MSK_BOOTSTRAP_SERVERS!],
  ssl: true,
  sasl: {
    mechanism: 'oauthbearer',
    oauthBearerProvider: async () => {
      const signer = new MSKAuthProvider({ region: 'us-east-1' });
      const token = await signer.tokenProvider();
      return { value: token.token };
    },
  },
});

// High-durability producer for probe results
const resultsProducer = kafka.producer({
  idempotent: true,            // enable.idempotence=true; forces acks=all, retries=MAX
  maxInFlightRequests: 5,      // required for idempotence
  compression: CompressionTypes.LZ4,
  // batch.size=65536 and linger.ms=5 via producer-level config:
  // Note: kafkajs sets these per send() call; for global defaults use
  // the KafkaJS 2.x createPartitioner + lingerMs option
});

// Low-latency producer for probe commands
const commandsProducer = kafka.producer({
  acks: 1,                     // leader only — commands are idempotent
  maxInFlightRequests: 10,     // higher throughput for command dispatch
  // no compression, default batch.size, linger.ms=0
});

// Send probe result with delivery timeout
await resultsProducer.send({
  topic: 'probe-results',
  timeout: 30000,              // delivery.timeout.ms bound
  messages: [{ key: endpointId, value: JSON.stringify(probeResult) }],
});

// Send probe command (fire-and-forget style, idempotent receiver)
await commandsProducer.send({
  topic: 'probe-commands',
  messages: [{ key: endpointId, value: JSON.stringify(probeCommand) }],
});

Cross-Pattern Failure Modes

FailurePatternSymptomResolution
MSK Serverless: consumer cannot commit offsets Pattern 1 Consumer re-reads from last committed offset after every restart; no error on acks but commits silently fail Add kafka-cluster:AlterGroup + kafka-cluster:DescribeGroup to consumer IAM policy
MSK Serverless: IAM token expiry Pattern 1 Connections drop every 15 minutes; MSKAuthProvider throws TokenExpiredException Verify MSKAuthProvider is called per-connection via oauthBearerProvider callback, not cached once at startup
Durability: duplicate probe results Pattern 2 Archiver inserts two rows per probe result; alert fires twice; dedup log shows PID mismatch on retry Enable idempotent: true on producer; verify maxInFlightRequests ≤ 5
Durability: NotEnoughReplicasException Pattern 2 Producer throws intermittently; probe results queue up in memory; memory pressure on probe worker Check MSK broker health in CloudWatch (UnderReplicatedPartitions metric); add min-broker alert; verify delivery.timeout.ms bounds retry window
Schema Registry: runtime deserialization failure Pattern 3 Consumer Lambda throws deserialization errors silently; archiver drops messages; probe results missing from verdict store Schema violation reached production; add schema-registration step to CI/CD pipeline on staging with compatibility check enabled
Schema Registry: cross-account GetSchemaVersion denied Pattern 3 Analytics consumer fails to deserialize; throws AccessDeniedException on Glue API call Attach resource-based policy on registry granting glue:GetSchemaVersion + glue:GetSchemaByDefinition + glue:QuerySchemaVersionMetadata to analytics account root or IAM role ARN
Producer: idempotence config conflict Pattern 4 Producer throws ConfigException: Must set acks to all at startup Remove explicit acks=1 from the idempotent producer config; idempotence enforces acks=all automatically
Producer: batching increases tail latency Pattern 4 P99 message latency increases from 5ms to 15ms when linger.ms=5 is added; probe commands delayed Keep linger.ms=0 on the probe-commands producer; apply batching tuning to probe-results producer only

Related guides