Guide · Kinesis Firehose S3 Delivery
Kinesis Firehose S3 Delivery Configuration for MCP Servers — Buffer Hints, Parquet Format Conversion, Compression
Kinesis Firehose's S3 delivery has five independently tunable dimensions — buffer size, buffer interval, S3 prefix, output format, and compression — and the right choices depend on whether you're optimizing for query cost, delivery latency, or storage cost. The defaults (5 MB / 300 seconds, plain JSON, UNCOMPRESSED) are fine for getting started but cost approximately 10× more in Athena query fees than a properly configured Parquet + Snappy pipeline on the same data. Three critical decisions: choose buffer hints based on your latency SLA (60 seconds minimum interval — use it when you need near-real-time Athena visibility; 300–900 seconds for cost-optimized batch archiving); enable format conversion to Parquet for any table you query more than once per day (Athena scans 5–10× less data on columnar Parquet vs row-oriented JSON); and use Hive-compatible S3 prefix patterns (year=YYYY/month=MM/day=DD/hour=HH) so Athena can use partition projection instead of requiring MSCK REPAIR TABLE after every delivery.
TL;DR
Set BufferingHints to 64 MB / 60 seconds for near-real-time Athena queries, or 128 MB / 900 seconds for cost-efficient batch archiving. Use S3 prefix probe-results/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/ for Hive partitioning. Enable DataFormatConversionConfiguration with Parquet output and a Glue Data Catalog schema for 5–10× Athena scan reduction. Use Snappy compression with Parquet (native block compression), GZIP for plain JSON (best ratio for text). Always set ErrorOutputPrefix separately — without it, error records land in the success prefix and corrupt your Athena table.
Buffer hint tuning: size vs interval tradeoffs
Firehose buffers records in memory and flushes to S3 when either the size limit or the interval limit is reached — whichever comes first. Lower limits mean more frequent, smaller S3 files; higher limits mean fewer, larger files.
| Parameter | Range | Effect of lower value | Effect of higher value |
|---|---|---|---|
| SizeInMBs | 1–128 MB | More frequent delivery; smaller S3 files; higher PUT cost | Less frequent delivery; larger S3 files; lower PUT cost |
| IntervalInSeconds | 60–900 seconds | Lower delivery latency; more files; higher PUT cost | Higher delivery latency (up to 15 min); fewer files; lower PUT cost |
Buffer presets for AliveMCP use cases:
| Use case | SizeInMBs | IntervalInSeconds | Rationale |
|---|---|---|---|
| Near-real-time Athena visibility | 64 | 60 | Data visible in Athena within ~2 minutes; 1,440 PUT operations/day/partition |
| Operational dashboard (5-min freshness) | 64 | 300 | Data visible within ~7 minutes; 288 PUTs/day/partition |
| Cost-optimized batch archive | 128 | 900 | Data visible within ~17 minutes; 96 PUTs/day/partition; 15× fewer files than 60s mode |
| Compliance archive (query-rare) | 128 | 900 | Minimize PUT operations; Athena queries infrequent anyway |
S3 PUT pricing is $0.005 per 1,000 requests. The cost difference between 60-second and 900-second buffer intervals is minimal in absolute dollars (approximately $0.001/day per partition difference for a single delivery stream). The more important consideration is S3 small-file fragmentation: thousands of 5–10 KB files per prefix cause Athena to spend most of its time on metadata operations rather than actual data scanning, increasing query time and cost even without increasing data volume.
S3 prefix patterns and Hive-compatible partitioning
The S3 prefix determines how Firehose organizes delivered objects. Athena and Glue perform best with Hive-compatible prefixes that use key=value format — these enable partition projection, eliminating manual partition maintenance.
// Hive-compatible prefix (recommended for Athena)
Prefix: 'probe-results/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/',
ErrorOutputPrefix: 'probe-results-errors/!{firehose:error-output-type}/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/',
// Standard datetime prefix (simpler; requires MSCK REPAIR TABLE for new partitions)
Prefix: 'probe-results/!{timestamp:yyyy/MM/dd/HH}/',
ErrorOutputPrefix: 'probe-results-errors/!{firehose:error-output-type}/!{timestamp:yyyy/MM/dd/HH}/',
With Hive-compatible prefixes, configure Athena partition projection on the Glue table to auto-detect new time partitions without any operator action:
-- Athena table with partition projection (no MSCK REPAIR TABLE needed)
CREATE EXTERNAL TABLE probe_results (
server_slug STRING,
tenant_id STRING,
status STRING,
response_time_ms BIGINT,
error_code STRING,
probed_at STRING
)
PARTITIONED BY (year STRING, month STRING, day STRING, hour STRING)
STORED AS PARQUET
LOCATION 's3://alivemcp-probe-archive/probe-results/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.year.type' = 'integer',
'projection.year.range' = '2026,2030',
'projection.year.digits' = '4',
'projection.month.type' = 'integer',
'projection.month.range' = '01,12',
'projection.month.digits' = '2',
'projection.day.type' = 'integer',
'projection.day.range' = '01,31',
'projection.day.digits' = '2',
'projection.hour.type' = 'integer',
'projection.hour.range' = '00,23',
'projection.hour.digits' = '2',
'storage.location.template'= 's3://alivemcp-probe-archive/probe-results/year=${year}/month=${month}/day=${day}/hour=${hour}/'
);
Format conversion: JSON to Parquet via Glue Data Catalog
Format conversion transforms incoming JSON records to Parquet (columnar) or ORC format before writing to S3. Athena scans 5–10× less data on a Parquet table with the same records as a JSON table, because columnar format allows Athena to read only the columns referenced in the query rather than parsing entire JSON records.
Requirements for format conversion:
- A Glue Data Catalog database and table with the schema matching your records
- An IAM role granting Firehose
glue:GetTableandglue:GetTableVersionspermissions on the Glue table - Source records must be JSON (not binary, not CSV)
- Format conversion is incompatible with dynamic partitioning inline parsing — if you use both, the Lambda preprocessor must handle both schema extraction and format normalization
// Enable format conversion in delivery stream configuration
ExtendedS3DestinationConfiguration: {
// ... prefix, buffer hints, etc. ...
DataFormatConversionConfiguration: {
Enabled: true,
InputFormatConfiguration: {
Deserializer: {
OpenXJsonSerDe: {}, // Parse input as JSON (handles nested objects, arrays)
// Alternative: HiveJsonSerDe for strict Hive JSON format
},
},
OutputFormatConfiguration: {
Serializer: {
ParquetSerDe: {
Compression: 'SNAPPY', // SNAPPY (fast, Parquet-native) or GZIP (better ratio)
BlockSizeBytes: 268435456, // 256 MB Parquet row group size (default)
PageSizeBytes: 65536, // 64 KB page size (default)
EnableDictionaryCompression: true, // Dictionary encoding for low-cardinality columns
WriterVersion: 'V2', // Parquet writer version (V2 improves null handling)
},
// Alternative: OrcSerDe for ORC format
},
},
SchemaConfiguration: {
RoleARN: 'arn:aws:iam::123456789012:role/firehose-glue-role',
DatabaseName: 'alivemcp',
TableName: 'probe_results',
Region: 'us-east-1',
VersionId: 'LATEST', // Always use latest Glue table schema version
},
},
}
Format conversion adds approximately 3–5 seconds of processing overhead per buffer flush. This is negligible for batch archiving workloads but means the effective minimum delivery latency is ~65 seconds even with a 60-second buffer interval.
Parquet vs ORC: which to choose
| Factor | Parquet | ORC |
|---|---|---|
| Athena support | Full (preferred format) | Full |
| Compression (Snappy) | ~3:1 ratio | ~3:1 ratio |
| Compression (Zlib/GZIP) | ~5:1 ratio | ~5:1 ratio |
| Read performance | Excellent for column-selective queries | Excellent; slightly faster for full-scan |
| Complex types (maps, structs) | Full support | Full support |
| Ecosystem compatibility | Wider (Spark, Flink, Pandas, Arrow) | Strong in Hive/Hadoop ecosystems |
| Recommendation for MCP data | Default choice | If migrating from existing Hive/Hadoop pipeline |
Compression options for JSON delivery (without format conversion)
When not using format conversion (delivering plain JSON or NDJSON), Firehose applies compression to the entire S3 object after buffering:
| Compression | Typical ratio on JSON | CPU cost | Athena compatibility | When to use |
|---|---|---|---|---|
| UNCOMPRESSED | 1:1 | None | Full | Development / debugging only |
| GZIP | ~8:1 on repetitive JSON | Low (CPU-cheap to decompress) | Full | Default for plain JSON — best balance of ratio and compatibility |
| SNAPPY | ~3:1 | Very low | Full (Athena supports SNAPPY) | When decompression speed matters more than ratio |
| HADOOP_SNAPPY | ~3:1 | Very low | Full | When downstream consumers use Hadoop snappy codec |
| ZIP | ~8:1 | Low | Partial (Athena cannot read ZIP) | Avoid for Athena-queried tables |
For AliveMCP probe results as plain JSON, GZIP is the recommended compression. A probe result record averages ~500 bytes uncompressed. With GZIP at 8:1 ratio, a 64 MB buffer holding 128,000 records compresses to ~8 MB — a significant reduction in S3 storage costs over months of data accumulation.
Important: do not use ZIP (ZIP) if the table will be queried by Athena — Athena cannot read ZIP-compressed S3 files directly and will fail with an unreadable format error. Use GZIP instead.
IAM permissions for Firehose S3 delivery
The Firehose service role needs the following S3 permissions on the destination bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FirehoseS3Delivery",
"Effect": "Allow",
"Action": [
"s3:AbortMultipartUpload",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::alivemcp-probe-archive",
"arn:aws:s3:::alivemcp-probe-archive/*"
]
},
{
"Sid": "GlueSchemaAccess",
"Effect": "Allow",
"Action": [
"glue:GetTable",
"glue:GetTableVersion",
"glue:GetTableVersions"
],
"Resource": [
"arn:aws:glue:us-east-1:123456789012:catalog",
"arn:aws:glue:us-east-1:123456789012:database/alivemcp",
"arn:aws:glue:us-east-1:123456789012:table/alivemcp/probe_results"
]
}
]
}
The GetObject and ListBucket permissions are required for Firehose to verify the S3 bucket is accessible during stream creation, not for the actual delivery. Omitting them causes the delivery stream creation to fail with a permission validation error even though the object PUT operation would succeed.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Athena query returns no results despite S3 data present | Partition projection range doesn't include current date, or S3 prefix doesn't match projection template | Verify projection range includes today's year; check prefix pattern matches storage.location.template exactly |
| Parquet conversion fails — all records to error prefix | Source JSON doesn't match Glue table schema (extra fields, type mismatch) | Use LATEST VersionId and update Glue table schema to match current record format; add nullable columns for new fields |
| S3 storage costs unexpectedly high despite GZIP | Parquet format chosen with GZIP but block sizes too small (many small row groups) | Increase BufferingHints SizeInMBs to at least 64 MB so each S3 file contains a full Parquet row group |
| Athena queries slow despite Parquet format | Too many small Parquet files (sub-1 MB), causing metadata overhead to dominate | Increase buffer size to 128 MB to produce fewer, larger Parquet files per hour |
| Error records appearing in success prefix | ErrorOutputPrefix not configured — defaults to writing errors to same prefix as successful delivery | Always set ErrorOutputPrefix to a separate path with !{firehose:error-output-type} |
| Delivery stream creation fails: access denied on S3 | Missing GetObject or ListBucket permissions on the delivery role | Add these permissions to the IAM role even though they aren't used at delivery time |