Intermediate
Introduction: Why Lambda Function URLs Deserve Your Attention
If you’ve ever set up an Amazon API Gateway just to expose a single Lambda function as an HTTP endpoint, you know the feeling: it works, but it feels like driving a semi-truck to the grocery store. API Gateway is powerful — throttling, request validation, API keys, usage plans, custom domains — but sometimes you just need an HTTPS endpoint that invokes a Lambda function, and you need it secured with IAM.
That’s exactly why AWS introduced Lambda Function URLs in April 2022. A Function URL gives your Lambda function a dedicated HTTPS endpoint without any intermediary service. When combined with IAM authentication (AWS_IAM auth type), you get a lightweight, secure, and cost-effective way to build internal APIs, service-to-service communication, and webhook receivers.
This article is for intermediate AWS developers and DevOps engineers who already know how Lambda works and want a practical, production-ready guide to using Function URLs with IAM auth. We’ll cover the architecture, setup, signing requests, cross-account access, common mistakes, and a real cost comparison against API Gateway.
Prerequisites
- An AWS account with permissions to create Lambda functions and IAM roles/policies
- AWS CLI v2 installed and configured (
aws --versionshould return 2.x) - Python 3.9+ installed (for the signing examples)
- Basic understanding of IAM policies and Lambda execution roles
- Familiarity with HTTP APIs and AWS Signature Version 4 concepts
Lambda Function URLs vs. API Gateway: When to Use Which
Before diving into implementation, let’s be clear about when Function URLs make sense and when they don’t.
| Feature | Lambda Function URL | API Gateway (REST/HTTP API) |
|---|---|---|
| Pricing | No additional cost (you pay only for Lambda invocations) | $1.00–$3.50 per million requests + Lambda costs |
| Auth options | AWS_IAM or NONE | IAM, Cognito, Lambda authorizers, API keys, OAuth/JWT (HTTP API) |
| Custom domain | Not natively supported (use CloudFront) | Natively supported |
| Throttling / Rate limiting | No built-in throttling (uses Lambda concurrency limits) | Built-in throttling per stage, per route, per API key |
| Request/Response transformation | None — raw request goes to Lambda | Mapping templates (REST API), parameter mapping (HTTP API) |
| WAF integration | Not directly (use CloudFront + WAF) | Supported (REST API, and HTTP API via association) |
| Latency | Lower (no intermediary hop) | Adds ~10-30ms overhead |
| Multiple routes | Single endpoint per function | Multiple routes to multiple integrations |
Use Lambda Function URLs when: you need a simple, single-purpose endpoint for service-to-service calls, internal tools, webhooks, or lightweight microservices where you control both the caller and the callee. Stick with API Gateway when: you need custom domains, rate limiting, request validation, multiple routes, Cognito auth, or public-facing APIs with WAF protection.
Step 1: Create a Lambda Function with a Function URL (IAM Auth)
Let’s start by creating a Lambda function and enabling a Function URL with IAM authentication. We’ll do this entirely with the AWS CLI.
Create the Lambda function
First, create a simple Python handler. Save this as lambda_function.py:
import json
def lambda_handler(event, context):
# Function URL events use a specific payload format (v2)
http_method = event.get("requestContext", {}).get("http", {}).get("method", "UNKNOWN")
path = event.get("rawPath", "/")
headers = event.get("headers", {})
body = event.get("body", None)
# Parse JSON body if present
parsed_body = None
if body:
try:
parsed_body = json.loads(body)
except json.JSONDecodeError:
parsed_body = body
response = {
"message": "Hello from Lambda Function URL with IAM Auth!",
"method": http_method,
"path": path,
"caller_identity": event.get("requestContext", {}).get("authorizer", {}).get("iam", {}),
"received_body": parsed_body
}
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": json.dumps(response, default=str)
}
Note that when IAM auth is enabled, the requestContext.authorizer.iam field is populated with the caller’s identity, including the accountId, userArn, and accessKey. This is extremely useful for auditing and authorization logic.
Package and deploy
# Zip the function
zip function.zip lambda_function.py
# Create an execution role (if you don't already have one)
aws iam create-role \
--role-name lambda-furl-demo-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach basic execution policy for CloudWatch Logs
aws iam attach-role-policy \
--role-name lambda-furl-demo-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Wait a few seconds for IAM propagation, then create the function
sleep 10
aws lambda create-function \
--function-name my-furl-demo \
--runtime python3.12 \
--role arn:aws:iam::YOUR_ACCOUNT_ID:role/lambda-furl-demo-role \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip
Create the Function URL with IAM auth
aws lambda create-function-url-config \
--function-name my-furl-demo \
--auth-type AWS_IAM
# Output will include the FunctionUrl, e.g.:
# "FunctionUrl": "https://abc123xyz.lambda-url.us-east-1.on.aws/"
That’s it. No API Gateway, no stages, no deployments. Your function now has an HTTPS endpoint. But because we set --auth-type AWS_IAM, any unauthenticated request will receive a 403 Forbidden response.
Step 2: Configure IAM Permissions for Invoking the Function URL
This is where most people get tripped up. With IAM auth enabled, the calling principal must have the lambda:InvokeFunctionUrl permission. This is a different action from lambda:InvokeFunction.
Resource-based policy (for cross-account or specific principals)
You can grant access via a resource-based policy on the Lambda function itself:
# Grant a specific IAM role permission to invoke the Function URL
aws lambda add-permission \
--function-name my-furl-demo \
--statement-id allow-my-service-role \
--action lambda:InvokeFunctionUrl \
--principal arn:aws:iam::123456789012:role/my-calling-service-role \
--function-url-auth-type AWS_IAM
The --function-url-auth-type AWS_IAM parameter is required when granting lambda:InvokeFunctionUrl — omitting it causes the command to fail.
Identity-based policy (attached to the caller’s IAM role/user)
Alternatively, attach this policy to the calling IAM role or user:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "lambda:InvokeFunctionUrl",
"Resource": "arn:aws:lambda:us-east-1:YOUR_ACCOUNT_ID:function:my-furl-demo"
}
]
}
Important: For same-account access, either a resource-based policy OR an identity-based policy is sufficient. For cross-account access, you need both — a resource-based policy on the function granting access to the external account, AND an identity-based policy in the calling account allowing the action. This is consistent with standard IAM cross-account access behavior.
Step 3: Invoke the Function URL with AWS Signature Version 4
Here’s the critical piece: because the Function URL uses IAM auth, every HTTP request must be signed with AWS Signature Version 4 (SigV4). You can’t just curl the URL — you need to sign the request with valid AWS credentials.
Option A: Using the AWS CLI (Quickest for testing)
The simplest way to test is with curl and the --aws-sigv4 flag (available in curl 7.75+):
# Set your Function URL
FUNCTION_URL="https://abc123xyz.lambda-url.us-east-1.on.aws/"
# Use curl with SigV4 signing
curl -X POST "$FUNCTION_URL" \
--aws-sigv4 "aws:amz:us-east-1:lambda" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
Note: If you’re using temporary credentials (e.g., from an assumed role), you also need to pass the session token. Curl’s --aws-sigv4 supports this via the x-amz-security-token header:
curl -X POST "$FUNCTION_URL" \
--aws-sigv4 "aws:amz:us-east-1:lambda" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
Option B: Python with the requests library and botocore
For production service-to-service calls, here’s a robust Python implementation:
import json
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.session import Session
def invoke_function_url(url, method="GET", payload=None, region="us-east-1"):
"""
Invoke a Lambda Function URL with IAM auth using SigV4 signing.
Uses the default credential chain (env vars, ~/.aws/credentials,
instance profile, ECS task role, etc.)
"""
session = Session()
credentials = session.get_credentials()
# Resolve credentials (handles temporary creds, AssumeRole, etc.)
credentials = credentials.get_frozen_credentials()
headers = {"Content-Type": "application/json"}
body = json.dumps(payload) if payload else None
# Create an AWSRequest object
request = AWSRequest(
method=method,
url=url,
data=body,
headers=headers
)
# Sign the request - service name is "lambda" for Function URLs
SigV4Auth(credentials, "lambda", region).add_auth(request)
# Send the signed request
response = requests.request(
method=method,
url=url,
headers=dict(request.headers),
data=body
)
return response
if __name__ == "__main__":
url = "https://abc123xyz.lambda-url.us-east-1.on.aws/"
response = invoke_function_url(
url=url,
method="POST",
payload={"message": "Hello from Python client"},
region="us-east-1"
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
Key detail: The service name for SigV4 signing is "lambda", not "execute-api" (which is what you’d use for API Gateway). Getting this wrong is one of the most common causes of 403 errors.
Option C: Calling from another Lambda function (service-to-service)
When calling a Function URL from another Lambda function, the calling function’s execution role needs the lambda:InvokeFunctionUrl permission. The signing works the same way — the Lambda runtime provides credentials via environment variables that the botocore session automatically picks up.
import json
import os
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.session import Session
from urllib import request as urllib_request
def lambda_handler(event, context):
"""
Calling a Function URL from another Lambda function.
Uses urllib (available in Lambda runtime) to avoid packaging requests.
"""
target_url = os.environ["TARGET_FUNCTION_URL"]
region = os.environ.get("AWS_REGION", "us-east-1")
session = Session()
credentials = session.get_credentials().get_frozen_credentials()
payload = json.dumps({"caller": "upstream-lambda", "data": event})
aws_request = AWSRequest(
method