Find Idle Kinesis Data Streams

Strands of fiber optic cable glowing blue and white against a dark background

Photo by John Adams on Unsplash

Idle Kinesis data stream cost is the charge a stream keeps paying with no traffic: $0.015 per shard-hour for a provisioned stream, plus extended retention and enhanced fan-out consumers, or $0.04 per stream-hour in on-demand mode (us-east-1, September 2026). Find idle streams by summing IncomingRecords and GetRecords.Records in CloudWatch over 14 days.

Kinesis streams outlive the projects that created them. The producer is switched off, the consumer is moved to a new stream, and the old one keeps its shards, its 7-day retention and its enhanced fan-out consumer, billed every hour. Unlike a stopped instance, an idle stream has no “stopped” state, so the idle Kinesis data stream cost never drops to zero until you change or delete it.

This example is for engineers reviewing streaming spend. It lists every stream in a Region, reads 14 days of write and read metrics from CloudWatch, and estimates the fixed monthly charge each stream carries. It changes nothing. For writing to streams, see the guide to Kinesis PutRecords with AWS SDK v3, which also compares on-demand and provisioned costs for busy streams.

What does an idle Kinesis data stream cost?

Only the time-based charges apply to a stream with no traffic. The per-GB and per-record charges are zero when nothing is written or read.

Charge (US East, N. Virginia) Provisioned On-demand (standard)
Base charge while the stream exists $0.015 per shard-hour $0.04 per stream-hour
Retention from 24 hours to 7 days $0.020 per shard-hour $0.10 per GB-month of stored data
Retention beyond 7 days $0.023 per GB-month $0.023 per GB-month
Enhanced fan-out consumer $0.015 per consumer-shard-hour, plus $0.013 per GB read $0.05 per GB read
Writes $0.014 per million 25 KB PUT payload units $0.08 per GB
Idle charges that remain Shards, extended retention, fan-out consumers Stream-hours only

Prices are as of September 2026, from the AWS Price List API file for Amazon Kinesis (published 11 September 2026) and the Kinesis Data Streams pricing page. The pricing page also lists an account-level On-demand Advantage mode with no per-stream-hour charge but a minimum throughput commitment; the script’s estimate assumes the standard modes above.

Worked example

From the sample output below, legacy-orders is a provisioned stream with 4 shards, 168-hour retention and 1 enhanced fan-out consumer. Using 730 hours per month:

  • Shards: 4 × $0.015 × 730 = $43.80
  • Extended retention: 4 × $0.020 × 730 = $58.40
  • Fan-out consumer: 1 consumer × 4 shards × $0.015 × 730 = $43.80
  • Total: $146.00 per month, with no data flowing

The on-demand stream iot-test costs $0.04 × 730 = $29.20 per month idle. Together that’s $175.20 a month, or $2,102.40 a year, for two streams nobody uses. Notice that extended retention costs more than the shards themselves here.

What does the script do?

  1. Lists streamspaginateListStreams, using the StreamSummaries it returns.
  2. Reads configurationDescribeStreamSummary for mode, OpenShardCount and RetentionPeriodHours, then paginateListStreamConsumers for enhanced fan-out consumer names.
  3. Sums trafficGetMetricData in the AWS/Kinesis namespace with one period covering the whole window: IncomingRecords for writes, GetRecords.Records for polling reads and SubscribeToShardEvent.Records per consumer for fan-out reads.
  4. Labels and pricesIDLE (no writes, no reads), no readers (writes nobody reads), no new data (reads but no writes) or active, with the fixed monthly charge from the table above.

These stream-level metrics are sent to CloudWatch every minute at no charge. Shard-level metrics are optional and billed as custom metrics, so an idle stream with enhanced monitoring switched on costs a little more than the script shows.

Prerequisites

  • Node.js 18 or later with tsx, plus @aws-sdk/client-kinesis and @aws-sdk/client-cloudwatch.
  • A read-only profile, set up as in the guide to AWS SDK v3 credential providers.
  • A window that covers your slowest consumer. A batch job that reads once a month needs a longer --days (up to 63).

Which IAM permissions does it need?

All read-only. ListStreams and GetMetricData don’t support resource-level permissions.

idle-kinesis-streams-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListStreamsAndMetrics",
      "Effect": "Allow",
      "Action": ["kinesis:ListStreams", "cloudwatch:GetMetricData"],
      "Resource": "*"
    },
    {
      "Sid": "DescribeStreams",
      "Effect": "Allow",
      "Action": ["kinesis:DescribeStreamSummary", "kinesis:ListStreamConsumers"],
      "Resource": "arn:aws:kinesis:us-east-1:111122223333:stream/*"
    }
  ]
}

AWS’s ReadOnlyAccess managed policy also covers these calls. To check a modified script, paste it into the IAM policy generator for TypeScript code.

The script to find idle Kinesis data streams

find-idle-kinesis-data-streams.ts

// find-idle-kinesis-data-streams.ts
// Lists Kinesis data streams in one Region, sums 14 days of writes (IncomingRecords), GetRecords reads and
// enhanced fan-out reads from CloudWatch, and estimates the fixed monthly charge each stream keeps paying.
// Report only: it never changes or deletes a stream.
// Usage:
//   npx tsx find-idle-kinesis-data-streams.ts [--region us-east-1] [--days 14]
import {
  KinesisClient,
  DescribeStreamSummaryCommand,
  paginateListStreamConsumers,
  paginateListStreams,
} from "@aws-sdk/client-kinesis";
import { CloudWatchClient, GetMetricDataCommand, type MetricDataQuery } from "@aws-sdk/client-cloudwatch";

const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
  const i = args.indexOf(name);
  return i >= 0 ? args[i + 1] : undefined;
};
const region = flag("--region") ?? process.env.AWS_REGION ?? "us-east-1";
const days = Number(flag("--days") ?? 14);
const HOURS_PER_MONTH = 730;
// us-east-1 prices from the AWS Price List API (AmazonKinesis offer), September 2026.
const PRICE = {
  shardHour: 0.015, // provisioned shard-hour
  extendedRetentionShardHour: 0.02, // provisioned, retention above 24 hours up to 7 days
  efoConsumerShardHour: 0.015, // provisioned, enhanced fan-out consumer-shard hour
  onDemandStreamHour: 0.04, // on-demand (standard) stream-hour
};
const kinesis = new KinesisClient({ region });
const cloudwatch = new CloudWatchClient({ region });

interface Row {
  Stream: string;
  Mode: string;
  Shards: number;
  RetentionH: number;
  EFO: number;
  Writes: number;
  Reads: number;
  Verdict: string;
  FixedUsdPerMonth: number;
}

const errText = (err: unknown): string => (err instanceof Error ? `${err.name}: ${err.message}` : String(err));

// Sums one metric per query over the whole window (a single period covering all the days).
async function sums(queries: { id: string; metric: string; dims: Record<string, string> }[]): Promise<Map<string, number>> {
  const end = new Date();
  const start = new Date(end.getTime() - days * 86_400_000);
  const MetricDataQueries: MetricDataQuery[] = queries.map((q) => ({
    Id: q.id,
    MetricStat: {
      Metric: {
        Namespace: "AWS/Kinesis",
        MetricName: q.metric,
        Dimensions: Object.entries(q.dims).map(([Name, Value]) => ({ Name, Value })),
      },
      Period: days * 86_400,
      Stat: "Sum",
    },
  }));
  const result = new Map<string, number>();
  let NextToken: string | undefined;
  do {
    const out = await cloudwatch.send(new GetMetricDataCommand({ StartTime: start, EndTime: end, MetricDataQueries, NextToken }));
    for (const r of out.MetricDataResults ?? []) {
      const total = (r.Values ?? []).reduce((a, b) => a + b, 0);
      result.set(r.Id ?? "", (result.get(r.Id ?? "") ?? 0) + total);
    }
    NextToken = out.NextToken;
  } while (NextToken);
  return result;
}

async function inspect(name: string): Promise<Row> {
  const s = (await kinesis.send(new DescribeStreamSummaryCommand({ StreamName: name }))).StreamDescriptionSummary;
  const mode = s?.StreamModeDetails?.StreamMode ?? "PROVISIONED";
  const shards = s?.OpenShardCount ?? 0;
  const retention = s?.RetentionPeriodHours ?? 24;
  const consumers: string[] = [];
  if (s?.StreamARN) {
    for await (const page of paginateListStreamConsumers({ client: kinesis }, { StreamARN: s.StreamARN })) {
      for (const c of page.Consumers ?? []) if (c.ConsumerName) consumers.push(c.ConsumerName);
    }
  }
  const queries = [
    { id: "writes", metric: "IncomingRecords", dims: { StreamName: name } },
    { id: "reads", metric: "GetRecords.Records", dims: { StreamName: name } },
    ...consumers.map((c, i) => ({ id: `efo${i}`, metric: "SubscribeToShardEvent.Records", dims: { StreamName: name, ConsumerName: c } })),
  ];
  const m = await sums(queries);
  const writes = m.get("writes") ?? 0;
  let reads = m.get("reads") ?? 0;
  consumers.forEach((_c, i) => (reads += m.get(`efo${i}`) ?? 0));

  let fixed: number;
  if (mode === "ON_DEMAND") {
    fixed = PRICE.onDemandStreamHour * HOURS_PER_MONTH;
  } else {
    fixed = shards * PRICE.shardHour * HOURS_PER_MONTH;
    if (retention > 24) fixed += shards * PRICE.extendedRetentionShardHour * HOURS_PER_MONTH;
    fixed += consumers.length * shards * PRICE.efoConsumerShardHour * HOURS_PER_MONTH;
  }
  const verdict = writes === 0 && reads === 0 ? "IDLE" : writes > 0 && reads === 0 ? "no readers" : writes === 0 ? "no new data" : "active";
  return {
    Stream: name, Mode: mode, Shards: shards, RetentionH: retention, EFO: consumers.length,
    Writes: writes, Reads: reads, Verdict: verdict, FixedUsdPerMonth: Number(fixed.toFixed(2)),
  };
}

async function main(): Promise<void> {
  if (!Number.isFinite(days) || days < 1 || days > 63) throw new Error("--days must be between 1 and 63");
  const rows: Row[] = [];
  for await (const page of paginateListStreams({ client: kinesis }, {})) {
    for (const s of page.StreamSummaries ?? []) {
      if (!s.StreamName) continue;
      try {
        rows.push(await inspect(s.StreamName));
      } catch (err) {
        console.error(`Skipping ${s.StreamName}: ${errText(err)}`);
      }
    }
  }
  rows.sort((a, b) => Number(b.Verdict === "IDLE") - Number(a.Verdict === "IDLE") || b.FixedUsdPerMonth - a.FixedUsdPerMonth);
  console.table(rows);
  const idle = rows.filter((r) => r.Verdict === "IDLE");
  const cost = idle.reduce((sum, r) => sum + r.FixedUsdPerMonth, 0);
  console.log(`${rows.length} streams in ${region}; ${idle.length} idle for ${days} days, about $${cost.toFixed(2)}/month in fixed charges. Nothing was changed.`);
}

main().catch((err) => {
  console.error(errText(err));
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-kinesis @aws-sdk/client-cloudwatch
npm install --save-dev tsx typescript @types/node

# Last 14 days in us-east-1
AWS_PROFILE=readonly npx tsx find-idle-kinesis-data-streams.ts --region us-east-1

# A longer window for monthly consumers
AWS_PROFILE=readonly npx tsx find-idle-kinesis-data-streams.ts --region us-east-1 --days 45

Sample output

Output

┌─────────┬─────────────────┬───────────────┬────────┬────────────┬─────┬──────────┬──────────┬──────────────┬──────────────────┐
│ (index) │ Stream          │ Mode          │ Shards │ RetentionH │ EFO │ Writes   │ Reads    │ Verdict      │ FixedUsdPerMonth │
├─────────┼─────────────────┼───────────────┼────────┼────────────┼─────┼──────────┼──────────┼──────────────┼──────────────────┤
│ 0       │ 'legacy-orders' │ 'PROVISIONED' │ 4      │ 168        │ 1   │ 0        │ 0        │ 'IDLE'       │ 146              │
│ 1       │ 'iot-test'      │ 'ON_DEMAND'   │ 4      │ 24         │ 0   │ 0        │ 0        │ 'IDLE'       │ 29.2             │
│ 2       │ 'clickstream'   │ 'ON_DEMAND'   │ 4      │ 24         │ 0   │ 48211930 │ 96420011 │ 'active'     │ 29.2             │
│ 3       │ 'audit-events'  │ 'PROVISIONED' │ 2      │ 24         │ 0   │ 120455   │ 0        │ 'no readers' │ 21.9             │
└─────────┴─────────────────┴───────────────┴────────┴────────────┴─────┴──────────┴──────────┴──────────────┴──────────────────┘
4 streams in us-east-1; 2 idle for 14 days, about $175.20/month in fixed charges. Nothing was changed.

Names are illustrative. clickstream has twice as many reads as writes because two applications consume it. audit-events is written to but never read: either a consumer is missing, or you’re paying to store records that expire unread after 24 hours.

What should you do with an idle stream?

The script is report-only on purpose: a stream’s producers and consumers live in other code, and deleting a stream deletes its data. Work through these in order:

  • Confirm the owners. Look for Lambda event source mappings, Firehose delivery streams and applications configured with the stream name. Tags help; the script to find untagged AWS resources shows which streams have none.
  • Cut the extras first. DecreaseStreamRetentionPeriod back to 24 hours removes the extended retention charge, and DeregisterStreamConsumer removes a fan-out consumer nobody reads from. On legacy-orders that alone saves $102.20 of the $146.00.
  • Delete it when you’re sure. DeleteStream removes the stream and every record in it. Set a budget alert with AWS SDK v3 first if you want a signal when streaming spend comes back.
  • Check the alarms. Alarms on a deleted stream stop receiving data and usually end up in INSUFFICIENT_DATA; the script to find CloudWatch alarms with insufficient data finds them afterwards.

The same idle-resource pattern applies to other always-on services: the scripts to find idle EMR clusters and find idle ElastiCache clusters work the same way.

Troubleshooting

  • Every stream shows 0 reads and writes. Check the Region. CloudWatch metrics are Regional, and a profile’s default Region may not be the one your streams live in.
  • A stream you know is used shows no reads. Its consumer may run less often than your window, such as a monthly batch job. Run again with a larger --days.
  • AccessDenied on GetMetricData. The profile has Kinesis read access but not CloudWatch. Add cloudwatch:GetMetricData, or see how to troubleshoot IAM AccessDenied errors.
  • Costs don’t match your bill. The estimate covers fixed charges only and assumes 730 hours per month and us-east-1 prices. Stored data beyond 7 days and enhanced monitoring are extra.

Ask ChatWithCloud instead

For a quick look, ask ChatWithCloud “Which Kinesis streams in us-east-1 had no incoming records in the last 14 days, and how many shards does each have?” It writes AWS SDK for JavaScript v2 code that calls Kinesis and CloudWatch, runs it on your machine with your profile and explains the result. It’s a good follow-up when you ask AI why your AWS bill increased and Kinesis shows up. See ChatWithCloud pricing and the free trial runs for the plans and the 15 free runs.

Frequently asked questions

What is the idle Kinesis data stream cost per month?

In us-east-1 as of September 2026, a provisioned shard costs $0.015 per hour, about $10.95 per month, and an on-demand stream costs $0.04 per hour, about $29.20 per month, before retention and fan-out charges.

Does an on-demand Kinesis stream cost money with no traffic?

Yes. On-demand standard mode charges per stream-hour while the stream exists, even with no data written or read.

Which CloudWatch metrics show whether a Kinesis stream is used?

IncomingRecords for writes, GetRecords.Records for polling consumers, and SubscribeToShardEvent.Records per consumer for enhanced fan-out. Sum each over the window.

Can I pause a Kinesis data stream?

No. There’s no stopped state. You can lower retention, remove fan-out consumers, reduce shards or delete the stream.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud