11.7 Lambda URLs: Direct HTTPS Endpoints Without API Gateway
Right, so you’ve been building these serverless APIs and you’ve probably noticed that the bill for API Gateway is starting to look like a car payment. Or maybe you just need a single, simple endpoint and the sheer, overwhelming heft of API Gateway feels like using a particle accelerator to crack an egg. Enter Lambda Function URLs. This is AWS finally giving us a direct line from the internet to our function, no bouncer required. It’s brilliantly simple, dangerously powerful, and in about five minutes, you’ll wonder how you lived without it for those smaller jobs.
Think of a Function URL as a public internet address that’s permanently wired directly to your Lambda’s invocation input. You send an HTTPS request to it, and it fires your function, passing in the entire request event. You get a full request context—headers, body, query string parameters, the works—and you can return a full HTTP response. It’s a first-class HTTP citizen, not just some event payload.
Why You’d Use This Instead of API Gateway
This isn’t an API Gateway killer; it’s a different tool. Use API Gateway when you need the full suite of API management: usage plans, custom authorizers, request validation, caching, and a million other enterprise-grade features. Use a Function URL when you need an endpoint. It’s perfect for webhooks, simple microservices, internal tools, or any situation where you just need to get data in and out without the ceremony. The cost model is the big win: you pay for Lambda execution, and that’s it. There’s no separate charge for millions of API calls. For high-throughput, simple services, that’s a game-changer.
Creating a URL and a Basic Handler
You can slap a Function URL onto any Lambda via the console, CLI, or Infrastructure-as-Code. Let’s do it with the AWS CLI and a Python handler. First, here’s a dead-simple function that echoes back some request info.
import json
def lambda_handler(event, context):
# Get the raw request path
request_path = event['requestContext']['http']['path']
# Get a query string parameter, defaulting to 'World'
name = event.get('queryStringParameters', {}).get('name', 'World')
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
},
'body': json.dumps({
'message': f'Hello, {name}!',
'path': request_path,
'secret_ingredient': context.aws_request_id
})
}
Now, to create the URL and wire it up. Notice the --auth-type. This is crucial.
# Create the function (assuming you've packaged the code above)
aws lambda create-function --function-name MyUrlFunction \
--runtime python3.9 --role arn:aws:iam::123456789012:role/lambda-exec-role \
--handler lambda_handler.lambda_function
# Now, add the Function URL with IAM authentication
aws lambda create-function-url-config --function-name MyUrlFunction \
--auth-type AWS_IAM
# Or, for a public endpoint (use with caution!), use NONE
aws lambda create-function-url-config --function-name MyUrlFunction \
--auth-type NONE
Boom. It’ll spit back a URL that looks like https://<random-id>.lambda-url.<region>.on.aws/. Hit it with curl and you’re in business.
The Almighty Auth-Type: IAM vs. NONE
This is the biggest “gotcha” and your first major decision. Choose AWS_IAM if you want the endpoint to be secured by AWS’s IAM system. This means callers must sign their requests with Sigv4, just like they’re calling any other AWS service. It’s secure, but it’s also a pain for anything outside the AWS ecosystem. You can’t just easily slap this URL into GitHub as a webhook.
Choose NONE if you want a truly public endpoint. Anyone on the internet with the URL can call it. This is, as you might guess, terrifying if you have any sensitive logic or data in that function. Never use auth-type: NONE without another form of protection. Which brings us to…
Securing Your Public Endpoint
If you go with NONE, you absolutely must implement your own authentication inside the function handler. The moment you set auth-type: NONE, AWS washes its hands of security. It’s all on you. Check for a secret token in the headers, validate a JWT, do something.
def lambda_handler(event, context):
# A classic, simple webhook auth pattern: a shared secret in a header
provided_token = event['headers'].get('x-auth-token')
if provided_token != 'my-super-secret-token':
return {
'statusCode': 401,
'body': json.dumps({'error': 'Unauthorized'})
}
# ... the rest of your logic if authorized
It’s not as clean as IAM, but it’s what you have to do. For anything even remotely important, AWS_IAM is the safer default.
The Rough Edges and Quirks
It’s not all sunshine. The event structure is different from API Gateway. It’s simpler, which is mostly good, but if you have tooling built around the APIG format, you’ll need to adapt. CORS is a manual configuration you have to set on the function URL config; you can’t define it in your response headers alone. And the biggest limitation: it’s one URL per function. You can’t route different paths (/users, /posts) to different handlers within the same function; it all goes to the same single handler. For that, you still need API Gateway or a framework like Express.js running inside your Lambda, which is a whole other can of worms.
So, in summary: Lambda URLs are your go-to for simplicity, cost-effectiveness, and getting a secure (if you use IAM) endpoint online in seconds. For anything that needs the full API management treatment, you still gotta pay the Gateway toll. Choose wisely.