Guide · Kinesis Firehose Error Handling

Kinesis Firehose Error Handling and Monitoring for MCP Servers — processing-failed Prefix, DataFreshness Alarm, Retry Duration

Firehose never fails a PutRecord or PutRecordBatch call to the producer — it always accepts the record. Instead, records that encounter transformation failures, format conversion failures, or destination delivery failures are silently diverted to a separate S3 error prefix and metered against CloudWatch metrics that nobody monitors by default. This silent failure model means you can lose weeks of probe result data before noticing — the API always returns HTTP 200, the stream always appears healthy in the console, but the processing-failed/ prefix is filling up with every record you've sent. Three monitoring essentials: alarm on DeliveryToS3.DataFreshness exceeding your buffer interval plus two minutes — this metric tracks the age of the oldest undelivered record and spikes immediately when delivery stalls; set a separate ErrorOutputPrefix so error records land in a queryable, dedicated path rather than mixed into your success data; and enable the backup S3 bucket with FailedDataOnly so you have a guaranteed copy of every failed record even if the error prefix configuration was wrong when the failure occurred.

TL;DR

Configure ErrorOutputPrefix to errors/!{firehose:error-output-type}/!{timestamp:yyyy/MM/dd/HH}/ — the !{firehose:error-output-type} token writes to processing-failed/, delivery-failed/, or backup-failed/ depending on the failure type. Alarm on DeliveryToS3.DataFreshness > (buffer_interval + 120) seconds. Enable S3 backup to FailedDataOnly for safety. Set RetryDuration to 300–900 seconds for S3 destination (longer for HTTP endpoint destinations). Monitor DeliveryToS3.ProcessingFailedRecords to detect transformation Lambda errors early.

The Firehose error routing model

Firehose routes failed records to the error prefix but never surfaces the failure to the producer. Understanding which failure type routes where is essential for setting up correct monitoring:

Failure typeError output type tokenS3 path exampleCause
Lambda transformation failureprocessing-failederrors/processing-failed/2026/10/30/14/Lambda error, timeout, Lambda returns ProcessingFailed, Lambda output >6 MB, base64 decode failure, record count mismatch
Format conversion failureprocessing-failederrors/processing-failed/2026/10/30/14/JSON schema mismatch with Glue table, malformed JSON, field type incompatibility
S3 delivery failuredelivery-failederrors/delivery-failed/2026/10/30/14/S3 bucket permission error, S3 bucket doesn't exist, S3 object key exceeds 1,024 bytes
Backup S3 failurebackup-failederrors/backup-failed/2026/10/30/14/Backup bucket permission error, backup bucket KMS key access failure

A subtle distinction: processing-failed is written when Firehose cannot process the record (transformation or format conversion error). delivery-failed is written when Firehose successfully processed the record but could not deliver it to the destination (e.g., S3 bucket deleted after stream creation). In practice, processing-failed is far more common in MCP probe pipelines because it catches every Lambda transformation bug.

ErrorOutputPrefix configuration

If ErrorOutputPrefix is not set, Firehose writes error records to the same S3 prefix as successful deliveries. This corrupts the Athena table (non-Parquet error payloads mixed into Parquet data files) and makes error records unfindable. Always set it explicitly:

// Delivery stream configuration
ExtendedS3DestinationConfiguration: {
  BucketARN: 'arn:aws:s3:::alivemcp-probe-archive',
  RoleARN: 'arn:aws:iam::123456789012:role/firehose-delivery-role',

  // Success prefix — Hive-compatible for Athena partition projection
  Prefix: 'probe-results/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/',

  // Error prefix — separate bucket path per error type
  // !{firehose:error-output-type} resolves to: processing-failed, delivery-failed, or backup-failed
  ErrorOutputPrefix: 'probe-errors/type=!{firehose:error-output-type}/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/',

  // ... buffer hints, format conversion, etc.
}

Error records written to the error prefix are the original source records (before transformation), base64-decoded, in the original format your producer sent. For AliveMCP, this means the raw JSON probe result is preserved in the error prefix — you can inspect it, fix the Lambda, and replay it by reading from the error prefix and writing back to the Firehose stream or the source Kinesis stream.

CloudWatch monitoring: the critical metrics

Firehose emits metrics to CloudWatch under the AWS/Firehose namespace. The minimal monitoring setup for a production AliveMCP pipeline:

MetricWhat it measuresAlarm thresholdWhy it matters
DeliveryToS3.DataFreshnessAge (seconds) of oldest record not yet delivered to S3> (buffer_interval + 120)Spikes when delivery is stalled; first sign of a problem
DeliveryToS3.Success1 if delivery succeeded, 0 if failed= 0 for ≥ 5 minutesHard delivery failure indicator
DeliveryToS3.ProcessingFailedRecordsCount of records that failed transformation/format conversion> 0 for ≥ 10 minutesCatches Lambda bugs before they accumulate
KinesisMillisBehindLatestConsumer lag from the source KDS stream (ms)> 60,000 ms (1 minute)Indicates Firehose is falling behind the source stream
DeliveryToS3.RecordsCount of successfully delivered recordsBelow expected baselineAlert if delivery rate drops unexpectedly (producer stopped)

The DeliveryToS3.DataFreshness alarm is the most important. Set the threshold to your buffer interval plus 2 minutes — for a 60-second buffer interval, alarm at >180 seconds; for a 300-second buffer interval, alarm at >420 seconds. If this metric exceeds the threshold, Firehose is not delivering records and the error prefix is accumulating data that must be replayed.

// CloudFormation / CDK snippet: DataFreshness alarm
{
  "Type": "AWS::CloudWatch::Alarm",
  "Properties": {
    "AlarmName": "alivemcp-firehose-delivery-stalled",
    "AlarmDescription": "Firehose delivery stalled — oldest record not delivered within buffer window",
    "Namespace": "AWS/Firehose",
    "MetricName": "DeliveryToS3.DataFreshness",
    "Dimensions": [{ "Name": "DeliveryStreamName", "Value": "alivemcp-probe-results" }],
    "Statistic": "Maximum",
    "Period": 60,
    "EvaluationPeriods": 3,
    "Threshold": 180,        // buffer_interval (60s) + 120s grace period
    "ComparisonOperator": "GreaterThanThreshold",
    "TreatMissingData": "notBreaching",  // No data = no records arriving = not a delivery failure
    "AlarmActions": ["arn:aws:sns:us-east-1:123456789012:alivemcp-ops-alerts"]
  }
}

Backup S3 bucket: safety net for failed records

Firehose supports a backup S3 destination that captures records before transformation and delivery. This provides a safety net if the transformation Lambda has a bug, the format conversion schema is wrong, or the destination bucket has a permission error — you always have the raw source records in the backup bucket to replay from.

ExtendedS3DestinationConfiguration: {
  // Primary delivery configuration...

  S3BackupMode: 'FailedDataOnly',  // 'Disabled' | 'FailedDataOnly' | 'Enabled' (all records)
  S3BackupConfiguration: {
    BucketARN: 'arn:aws:s3:::alivemcp-probe-backup',
    RoleARN: 'arn:aws:iam::123456789012:role/firehose-delivery-role',
    Prefix: 'raw-backup/!{timestamp:yyyy/MM/dd/HH}/',
    ErrorOutputPrefix: 'raw-backup-errors/!{firehose:error-output-type}/!{timestamp:yyyy/MM/dd/HH}/',
    BufferingHints: {
      SizeInMBs: 128,
      IntervalInSeconds: 900,  // Backup can use longer intervals — it's a safety net, not a query source
    },
    CompressionFormat: 'GZIP',
  },
}

Backup modes:

Retry duration configuration

Firehose retries failed S3 deliveries for a configurable duration before routing to the error prefix. This applies to destination delivery failures (S3 unavailable, permission error) not transformation failures (those are retried by Lambda's own retry mechanism, not Firehose's retry duration).

ExtendedS3DestinationConfiguration: {
  // S3 delivery has no separate RetryOptions — Firehose retries S3 deliveries
  // indefinitely until the RetryDuration expires (for S3 destination, the default
  // is effectively unlimited — S3 is highly available and rarely causes permanent failures)

  // For HTTP endpoint destinations, configure retry duration explicitly:
  // HttpEndpointDestinationConfiguration: {
  //   RetryOptions: {
  //     DurationInSeconds: 300,  // Retry HTTP endpoint failures for 5 minutes before error prefix
  //   },
  // }
}

For S3 destinations, Firehose retries delivery with exponential backoff for up to 24 hours before permanently routing to the error prefix. In practice, S3 delivery failures are almost always transient permission issues or bucket policy changes that are corrected within minutes — the 24-hour retry window means no data is lost unless the issue persists for more than a day.

For HTTP endpoint destinations (webhooks, custom analytics receivers), set RetryOptions.DurationInSeconds based on your endpoint's expected recovery time. A value of 300 seconds (5 minutes) is appropriate for ephemeral endpoint failures; use 3,600 seconds (1 hour) if the endpoint runs on infrastructure that might be down for routine maintenance windows.

Replaying failed records from the error prefix

When records accumulate in the processing-failed/ prefix due to a Lambda bug, the fix requires three steps: fix the Lambda, then replay the failed records through the corrected pipeline:

import { FirehoseClient, PutRecordBatchCommand } from '@aws-sdk/client-firehose';
import { S3Client, GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';

const s3 = new S3Client({ region: 'us-east-1' });
const firehose = new FirehoseClient({ region: 'us-east-1' });

async function replayFailedRecords(
  bucket: string,
  prefix: string,
  deliveryStreamName: string
): Promise {
  // List all error objects in the specified time range
  const list = await s3.send(new ListObjectsV2Command({
    Bucket: bucket,
    Prefix: prefix,  // e.g., 'probe-errors/type=processing-failed/year=2026/month=10/day=30/'
  }));

  for (const obj of list.Contents ?? []) {
    // Download and decompress the error file
    const get = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: obj.Key! }));
    const body = await get.Body!.transformToString('utf-8');

    // Each line is one original record (NDJSON format in error prefix)
    const records = body.trim().split('\n').map(line => ({
      Data: Buffer.from(line, 'utf-8'),
    }));

    // Replay in batches of 500 (PutRecordBatch limit)
    for (let i = 0; i < records.length; i += 500) {
      const batch = records.slice(i, i + 500);
      await firehose.send(new PutRecordBatchCommand({
        DeliveryStreamName: deliveryStreamName,
        Records: batch,
      }));
      // Firehose throttle: 1,000 records/s per delivery stream
      await new Promise(r => setTimeout(r, 500));
    }
  }
}

Replay considerations for AliveMCP probe data:

Complete failure mode reference

SymptomRoot causeDetectionFix
Records accumulating in processing-failed/ silentlyLambda transformation bug — returns ProcessingFailed or throwsDeliveryToS3.ProcessingFailedRecords > 0 CloudWatch alarmFix Lambda; replay from error prefix
DataFreshness metric spiking to 1,000+ secondsS3 delivery stalled — permission error, bucket policy change, or S3 outageDeliveryToS3.DataFreshness > threshold alarmRestore bucket permissions; records buffered in Firehose will deliver automatically when fixed
Format conversion errors for all records after schema changeGlue table schema out of sync with record format (new field added to probe results)DeliveryToS3.ProcessingFailedRecords spikes after schema change deploymentUpdate Glue table schema to add new field as nullable; Firehose uses LATEST version automatically
Error records in same prefix as success recordsErrorOutputPrefix not configuredAthena parse errors on the table; Parquet reader fails on non-Parquet error payloadsUpdate delivery stream with separate ErrorOutputPrefix; new errors will route correctly
KinesisMillisBehindLatest growing continuouslyFirehose consuming KDS slower than records are being produced (backpressure)KinesisMillisBehindLatest > 60,000 ms alarmCheck if Firehose transformation Lambda is slow; reduce buffer intervals to flush more frequently; check Lambda concurrency limits
PutRecordBatch returns FailedPutCount > 0 from producerFirehose delivery stream throttled (5,000 records/s or 5 MB/s per delivery stream limit)Firehose IncomingRecords vs SucceededRecords divergenceImplement exponential backoff and retry for FailedPutCount records in the producer; reduce burst rate
Backup bucket not receiving failed recordsS3BackupMode set to Disabled, or backup bucket IAM permissions missingRecords lost after transformation failure with no recovery pathEnable S3BackupMode: FailedDataOnly; verify backup bucket IAM policy grants Firehose PutObject