Skip to content

Instantly share code, notes, and snippets.

@vladaman
Last active August 2, 2025 19:35
Show Gist options
  • Save vladaman/3d7074d3e80cd42bf3606f80e0f8ff8f to your computer and use it in GitHub Desktop.
Save vladaman/3d7074d3e80cd42bf3606f80e0f8ff8f to your computer and use it in GitHub Desktop.
Lambda Cost Metrics Script: Analyze AWS Lambda costs, invocations, and duration with a 3-month trend table. Optimize your serverless spending with clear, actionable insights. AWS Lambda CloudWatch CostOptimization Serverless
#!/bin/bash
# Usage: get_lambda_metrics.sh <lambda_function_name>
# Example output
# -------------------------------------------------------------------------------------
# | Jun 2025 | Jul 2025 | Aug | MoM Trend
# Total | $1.323 | $4.060 | $0.809 | ▲ 206%
# Invocations | 1036005 | 3246359 | 412227 | ▲ 213%
# Duration (s) | 326839.058 | 999280.838 | 212742.010 | ▲ 205%
# -------------+-----------------+-----------------+-----------------+-----------------
# Check if the Lambda function name is provided as an argument
if [ -z "$1" ]; then
echo "Error: Lambda function name is required as an argument."
echo "Usage: get_lambda_metrics.sh <lambda_function_name>"
exit 1
fi
# Lambda function name
LAMBDA_FUNCTION_NAME="$1"
# Pricing information (from TODO.md)
PRICE_PER_GB_RAM_SECOND=0.0000133334
PRICE_PER_MILLION_REQUESTS=0.20
# Memory allocated to the Lambda function (in GB). You may need to adjust this value.
MEMORY_GB=0.256 # 256MB
# Function to get Lambda metrics for a given month
get_metrics() {
local month=$1
local year=$2
# Get the last day of the month
LAST_DAY=$(date -d "$year-$month-01 +1 month -1 day" +%d)
# Time range for the given month
START_TIME=$(date -Is -d "$year-$month-01T00:00:00Z" | tr -d ' ') # Remove spaces
END_TIME=$(date -Is -d "$year-$month-${LAST_DAY}T23:59:59Z" | tr -d ' ') # Remove spaces
# Get the total invocations for the given month
INVOCATIONS_JSON=$(aws cloudwatch get-metric-statistics \
--profile default \
--metric-name Invocations \
--namespace "AWS/Lambda" \
--dimensions Name=FunctionName,Value="$LAMBDA_FUNCTION_NAME" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 86400 \
--statistic Sum \
--output json)
INVOCATIONS=$(echo "$INVOCATIONS_JSON" | jq '[.Datapoints[].Sum] | add' 2>/dev/null || echo "0")
if [ -z "$INVOCATIONS" ] || [ "$INVOCATIONS" = "null" ]; then
INVOCATIONS=0
fi
# Get the total duration (in milliseconds) for the given month
DURATION_JSON=$(aws cloudwatch get-metric-statistics \
--profile default \
--metric-name Duration \
--namespace "AWS/Lambda" \
--dimensions Name=FunctionName,Value="$LAMBDA_FUNCTION_NAME" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 86400 \
--statistic Sum \
--output json)
DURATION=$(echo "$DURATION_JSON" | jq '[.Datapoints[].Sum] | add' 2>/dev/null || echo "0")
if [ -z "$DURATION" ] || [ "$DURATION" = "null" ]; then
DURATION=0
fi
# Convert duration from milliseconds to seconds
if [ -z "$DURATION" ] || [ "$DURATION" = "0" ]; then
DURATION_SECONDS=0
else
DURATION_SECONDS=$(echo "scale=3; $DURATION / 1000" | bc 2>/dev/null || echo "0")
fi
# Calculate the cost of invocations
if [ -z "$INVOCATIONS" ] || [ "$INVOCATIONS" = "0" ]; then
INVOCATIONS_MILLIONS=0
INVOCATIONS_COST=0
else
INVOCATIONS_MILLIONS=$(echo "scale=6; $INVOCATIONS / 1000000" | bc 2>/dev/null || echo "0")
INVOCATIONS_COST=$(echo "scale=6; $INVOCATIONS_MILLIONS * $PRICE_PER_MILLION_REQUESTS" | bc 2>/dev/null || echo "0")
fi
# Calculate the cost of duration
if [ -z "$DURATION_SECONDS" ] || [ "$DURATION_SECONDS" = "0" ]; then
DURATION_COST=0
else
DURATION_COST=$(echo "scale=6; $DURATION_SECONDS * $MEMORY_GB * $PRICE_PER_GB_RAM_SECOND" | bc 2>/dev/null || echo "0")
fi
# Calculate the total cost
if [ -z "$INVOCATIONS_COST" ] || [ -z "$DURATION_COST" ] || ([ "$INVOCATIONS_COST" = "0" ] && [ "$DURATION_COST" = "0" ]); then
TOTAL_COST=0
else
TOTAL_COST=$(echo "scale=6; $INVOCATIONS_COST + $DURATION_COST" | bc 2>/dev/null || echo "0")
fi
# Output detailed information to stderr
echo "Month: $month/$year" >&2
echo " Invocations: $INVOCATIONS" >&2
echo " Duration (seconds): $DURATION_SECONDS" >&2
echo " Invocations Cost: $INVOCATIONS_COST" >&2
echo " Duration Cost: $DURATION_COST" >&2
echo " Total Cost: $TOTAL_COST" >&2
# Return metrics as JSON
echo "{\"total_cost\":\"$TOTAL_COST\",\"invocations\":\"$INVOCATIONS\",\"duration_seconds\":\"$DURATION_SECONDS\"}"
}
# Get the current year and month
CURRENT_YEAR=$(date +%Y)
CURRENT_MONTH=$(date +%m)
CURRENT_MONTH_NAME=$(date -d "$CURRENT_YEAR-$CURRENT_MONTH-01" +%b)
# Get the previous year and month
PREVIOUS_MONTH=$(( (10#$CURRENT_MONTH - 1 + 12) % 12 ))
PREVIOUS_YEAR=$(( $CURRENT_YEAR - ($PREVIOUS_MONTH == 0) ))
if [ $PREVIOUS_MONTH -eq 0 ]; then
PREVIOUS_MONTH=12
fi
PREVIOUS_MONTH_NAME=$(date -d "$PREVIOUS_YEAR-$PREVIOUS_MONTH-01" +%b)
# Get the month before previous month
TWO_MONTHS_AGO_MONTH=$(( (10#$PREVIOUS_MONTH - 1 + 12) % 12 ))
TWO_MONTHS_AGO_YEAR=$(( $PREVIOUS_YEAR - ($TWO_MONTHS_AGO_MONTH == 0) ))
if [ $TWO_MONTHS_AGO_MONTH -eq 0 ]; then
TWO_MONTHS_AGO_MONTH=12
fi
TWO_MONTHS_AGO_MONTH_NAME=$(date -d "$TWO_MONTHS_AGO_YEAR-$TWO_MONTHS_AGO_MONTH-01" +%b)
# Get the metrics for the current month
CURRENT_MONTH_METRICS=$(get_metrics $CURRENT_MONTH $CURRENT_YEAR)
CURRENT_MONTH_COST=$(echo "$CURRENT_MONTH_METRICS" | jq -r '.total_cost')
CURRENT_MONTH_INVOCATIONS=$(echo "$CURRENT_MONTH_METRICS" | jq -r '.invocations')
CURRENT_MONTH_DURATION=$(echo "$CURRENT_MONTH_METRICS" | jq -r '.duration_seconds')
# Get the metrics for the previous month
PREVIOUS_MONTH_METRICS=$(get_metrics $PREVIOUS_MONTH $PREVIOUS_YEAR)
PREVIOUS_MONTH_COST=$(echo "$PREVIOUS_MONTH_METRICS" | jq -r '.total_cost')
PREVIOUS_MONTH_INVOCATIONS=$(echo "$PREVIOUS_MONTH_METRICS" | jq -r '.invocations')
PREVIOUS_MONTH_DURATION=$(echo "$PREVIOUS_MONTH_METRICS" | jq -r '.duration_seconds')
# Get the metrics for two months ago
TWO_MONTHS_AGO_METRICS=$(get_metrics $TWO_MONTHS_AGO_MONTH $TWO_MONTHS_AGO_YEAR)
TWO_MONTHS_AGO_COST=$(echo "$TWO_MONTHS_AGO_METRICS" | jq -r '.total_cost')
TWO_MONTHS_AGO_INVOCATIONS=$(echo "$TWO_MONTHS_AGO_METRICS" | jq -r '.invocations')
TWO_MONTHS_AGO_DURATION=$(echo "$TWO_MONTHS_AGO_METRICS" | jq -r '.duration_seconds')
# Function to calculate trend
calculate_trend() {
local prev_val=$1
local curr_val=$2
if [ -z "$prev_val" ] || [ "$prev_val" = "0" ] || [ -z "$curr_val" ] || [ "$curr_val" = "0" ]; then
echo "N/A"
return
fi
local diff=$(echo "scale=6; $curr_val - $prev_val" | bc 2>/dev/null || echo "0")
local percentage=0
if [ "$prev_val" != "0" ]; then
percentage=$(printf "%.0f" $(echo "scale=2; ($diff / $prev_val) * 100" | bc 2>/dev/null || echo "0"))
fi
local icon=""
if (( $(echo "$diff > 0" | bc -l) )); then
icon="▲" # Up arrow
elif (( $(echo "$diff < 0" | bc -l) )); then
icon="▼" # Down arrow
else
icon="━" # No change
fi
echo "${icon} $(echo "$percentage" | sed 's/^-//')%"
}
# Calculate trends (Previous Month vs Two Months Ago)
COST_TREND_TWO_MONTHS_AGO_TO_PREV=$(calculate_trend "$TWO_MONTHS_AGO_COST" "$PREVIOUS_MONTH_COST")
INVOCATIONS_TREND_TWO_MONTHS_AGO_TO_PREV=$(calculate_trend "$TWO_MONTHS_AGO_INVOCATIONS" "$PREVIOUS_MONTH_INVOCATIONS")
DURATION_TREND_TWO_MONTHS_AGO_TO_PREV=$(calculate_trend "$TWO_MONTHS_AGO_DURATION" "$PREVIOUS_MONTH_DURATION")
# Round the costs to 3 decimal places
TWO_MONTHS_AGO_COST_ROUNDED=$(printf "%.3f" "$TWO_MONTHS_AGO_COST")
PREVIOUS_MONTH_COST_ROUNDED=$(printf "%.3f" "$PREVIOUS_MONTH_COST")
CURRENT_MONTH_COST_ROUNDED=$(printf "%.3f" "$CURRENT_MONTH_COST")
# Display results in a table format
echo
printf "%-12s | %15s | %15s | %15s | %15s\n" "" "$TWO_MONTHS_AGO_MONTH_NAME $TWO_MONTHS_AGO_YEAR" "$PREVIOUS_MONTH_NAME $PREVIOUS_YEAR" "$CURRENT_MONTH_NAME $CURRENT_MONTH_YEAR" "MoM Trend"
printf "%-12s | %15s | %15s | %15s | %15s\n" "Total" "\$$TWO_MONTHS_AGO_COST_ROUNDED" "\$$PREVIOUS_MONTH_COST_ROUNDED" "\$$CURRENT_MONTH_COST_ROUNDED" "$COST_TREND_TWO_MONTHS_AGO_TO_PREV"
printf "%-12s | %15s | %15s | %15s | %15s\n" "Invocations" "$TWO_MONTHS_AGO_INVOCATIONS" "$PREVIOUS_MONTH_INVOCATIONS" "$CURRENT_MONTH_INVOCATIONS" "$INVOCATIONS_TREND_TWO_MONTHS_AGO_TO_PREV"
printf "%-12s | %15s | %15s | %15s | %15s\n" "Duration (s)" "$(printf "%.3f" "$TWO_MONTHS_AGO_DURATION")" "$(printf "%.3f" "$PREVIOUS_MONTH_DURATION")" "$(printf "%.3f" "$CURRENT_MONTH_DURATION")" "$DURATION_TREND_TWO_MONTHS_AGO_TO_PREV"
#!/bin/bash
# Optimized Lambda Top Functions Script
# Example output
# Function Name Total Cost Invocations Total Dur(ms) Memory Avg Dur(ms) Error% Errors Runtime
# ----------------------------------- ------------ ------------ ------------ -------- ---------- -------- -------- ---------------
# fxxxxxx-prod $0.0731 4.4K 34.6M 128MB 7734ms 99.00 % 4.4K nodejs20.x
# fxxxxxx-prod $0.0457 110.3K 5.6M 256MB 51ms 0.00 % 0 nodejs20.x
# fxxxxxx-prod $0.0018 3.9K 461.2K 128MB 115ms 0.00 % 0 provided.al2
set -euo pipefail
# Configuration
PRICE_PER_GB_SECOND=0.0000166667
PRICE_PER_MILLION_REQUESTS=0.20
DEFAULT_MEMORY_MB=128
DAYS_BACK=${1:-7} # Default to 7 days, can be overridden
MAX_FUNCTIONS=${2:-50} # Limit to top 50 functions by default
AWS_PROFILE="default"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_color() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to format numbers
format_number() {
local num=$1
if (( $(echo "$num >= 1000000" | bc -l) )); then
printf "%.1fM" $(echo "scale=1; $num / 1000000" | bc -l)
elif (( $(echo "$num >= 1000" | bc -l) )); then
printf "%.1fK" $(echo "scale=1; $num / 1000" | bc -l)
else
printf "%.0f" $num
fi
}
# Function to format cost
format_cost() {
local cost=$1
if (( $(echo "$cost >= 1" | bc -l) )); then
printf "\$%.2f" $cost
else
printf "\$%.4f" $cost
fi
}
print_color $BLUE "🚀 Fetching Lambda functions and metrics (last $DAYS_BACK days)..."
# Calculate time range
START_TIME=$(date -d "$DAYS_BACK days ago" -u '+%Y-%m-%dT%H:%M:%SZ')
END_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
# Create temporary files for processing
TEMP_DIR=$(mktemp -d)
FUNCTIONS_FILE="$TEMP_DIR/functions.txt"
RESULTS_FILE="$TEMP_DIR/results.txt"
# Cleanup function
cleanup() {
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
print_color $YELLOW "📋 Getting list of Lambda functions..."
# Fetch all Lambda function names and basic info in one call
aws lambda list-functions \
--profile "$AWS_PROFILE" \
--query 'Functions[].[FunctionName,MemorySize,Runtime]' \
--output text > "$FUNCTIONS_FILE"
TOTAL_FUNCTIONS=$(wc -l < "$FUNCTIONS_FILE")
print_color $GREEN "Found $TOTAL_FUNCTIONS Lambda functions"
if [ "$TOTAL_FUNCTIONS" -eq 0 ]; then
print_color $RED "❌ No Lambda functions found. Check your AWS credentials and region."
exit 1
fi
# Function to process a single Lambda function
process_function() {
local function_info="$1"
local function_name=$(echo "$function_info" | cut -f1)
local memory_mb=$(echo "$function_info" | cut -f2)
local runtime=$(echo "$function_info" | cut -f3)
# Set default memory if not available
if [[ -z "$memory_mb" || "$memory_mb" == "None" ]]; then
memory_mb=$DEFAULT_MEMORY_MB
fi
# Get invocations for the time period
local invocations=$(aws cloudwatch get-metric-statistics \
--profile "$AWS_PROFILE" \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value="$function_name" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 86400 \
--statistic Sum \
--query 'Datapoints[].Sum | [0]' \
--output text 2>/dev/null || echo "0")
# Get total duration for the time period
local duration_ms=$(aws cloudwatch get-metric-statistics \
--profile "$AWS_PROFILE" \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value="$function_name" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 86400 \
--statistic Sum \
--query 'Datapoints[].Sum | [0]' \
--output text 2>/dev/null || echo "0")
# Get errors for the time period
local errors=$(aws cloudwatch get-metric-statistics \
--profile "$AWS_PROFILE" \
--namespace AWS/Lambda \
--metric-name Errors \
--dimensions Name=FunctionName,Value="$function_name" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 86400 \
--statistic Sum \
--query 'Datapoints[].Sum | [0]' \
--output text 2>/dev/null || echo "0")
# Handle None values
[[ "$invocations" == "None" ]] && invocations="0"
[[ "$duration_ms" == "None" ]] && duration_ms="0"
[[ "$errors" == "None" ]] && errors="0"
# Ensure numeric values
invocations=$(echo "$invocations + 0" | bc -l 2>/dev/null || echo "0")
duration_ms=$(echo "$duration_ms + 0" | bc -l 2>/dev/null || echo "0")
errors=$(echo "$errors + 0" | bc -l 2>/dev/null || echo "0")
# Skip functions with no activity
if (( $(echo "$invocations == 0" | bc -l) )); then
return
fi
# Calculate costs
local duration_gb_seconds=$(echo "scale=9; ($duration_ms / 1000) * ($memory_mb / 1024)" | bc -l)
local invocation_cost=$(echo "scale=9; ($invocations / 1000000) * $PRICE_PER_MILLION_REQUESTS" | bc -l)
local compute_cost=$(echo "scale=9; $duration_gb_seconds * $PRICE_PER_GB_SECOND" | bc -l)
local total_cost=$(echo "scale=9; $invocation_cost + $compute_cost" | bc -l)
# Calculate error rate
local error_rate=0
if (( $(echo "$invocations > 0" | bc -l) )); then
error_rate=$(echo "scale=2; ($errors / $invocations) * 100" | bc -l)
fi
# Calculate average duration per invocation
local avg_duration=0
if (( $(echo "$invocations > 0" | bc -l) )); then
avg_duration=$(echo "scale=0; $duration_ms / $invocations" | bc -l)
fi
# Output results
printf "%s|%.6f|%.0f|%.0f|%s|%.0f|%.2f|%.0f|%s\n" \
"$function_name" "$total_cost" "$invocations" "$duration_ms" \
"$memory_mb" "$avg_duration" "$error_rate" "$errors" "$runtime"
}
print_color $YELLOW "⚡ Processing functions (this may take a few minutes)..."
# Process functions sequentially to avoid API throttling
counter=0
while IFS=$'\t' read -r function_name memory_mb runtime; do
counter=$((counter + 1))
if [ $counter -gt $MAX_FUNCTIONS ]; then
break
fi
echo "Processing $counter/$MAX_FUNCTIONS: $function_name" >&2
process_function "$function_name $memory_mb $runtime"
done < "$FUNCTIONS_FILE" > "$RESULTS_FILE"
# Check if we have any results
ACTIVE_FUNCTIONS=$(wc -l < "$RESULTS_FILE")
if [ "$ACTIVE_FUNCTIONS" -eq 0 ]; then
print_color $YELLOW "⚠️ No active Lambda functions found in the last $DAYS_BACK days."
print_color $BLUE "Try increasing the time range or check if your functions have been invoked recently."
exit 0
fi
# Sort by total cost (descending) and display results
print_color $GREEN "\n📊 Top Lambda Functions by Cost and Usage (Last $DAYS_BACK days):"
echo
printf "%-35s %-12s %-12s %-12s %-8s %-10s %-8s %-8s %-15s\n" \
"Function Name" "Total Cost" "Invocations" "Total Dur(ms)" "Memory" "Avg Dur(ms)" "Error%" "Errors" "Runtime"
printf "%-35s %-12s %-12s %-12s %-8s %-10s %-8s %-8s %-15s\n" \
"$(printf '%*s' 35 '' | tr ' ' '-')" \
"$(printf '%*s' 12 '' | tr ' ' '-')" \
"$(printf '%*s' 12 '' | tr ' ' '-')" \
"$(printf '%*s' 12 '' | tr ' ' '-')" \
"$(printf '%*s' 8 '' | tr ' ' '-')" \
"$(printf '%*s' 10 '' | tr ' ' '-')" \
"$(printf '%*s' 8 '' | tr ' ' '-')" \
"$(printf '%*s' 8 '' | tr ' ' '-')" \
"$(printf '%*s' 15 '' | tr ' ' '-')"
# Sort by cost and format output
sort -t'|' -k2 -nr "$RESULTS_FILE" | head -20 | while IFS='|' read -r name cost invocations duration memory avg_duration error_rate errors runtime; do
# Color coding based on cost and error rate
color=$NC
if (( $(echo "$cost > 1" | bc -l) )); then
color=$RED
elif (( $(echo "$cost > 0.1" | bc -l) )); then
color=$YELLOW
fi
if (( $(echo "$error_rate > 5" | bc -l) )); then
color=$RED
fi
printf "${color}%-35s %-12s %-12s %-12s %-8s %-10s %-8.2f%% %-8s %-15s${NC}\n" \
"${name:0:34}" \
"$(format_cost $cost)" \
"$(format_number $invocations)" \
"$(format_number $duration)" \
"${memory}MB" \
"${avg_duration}ms" \
"$error_rate" \
"$(format_number $errors)" \
"$runtime"
done
# Summary statistics
print_color $BLUE "\n📈 Summary:"
TOTAL_COST=$(awk -F'|' '{sum += $2} END {printf "%.4f", sum}' "$RESULTS_FILE")
TOTAL_INVOCATIONS=$(awk -F'|' '{sum += $3} END {printf "%.0f", sum}' "$RESULTS_FILE")
echo "Total Cost (${DAYS_BACK} days): $(format_cost $TOTAL_COST)"
echo "Total Invocations: $(format_number $TOTAL_INVOCATIONS)"
echo "Active Functions: $ACTIVE_FUNCTIONS"
print_color $GREEN "\n✅ Analysis complete!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment