API Gateway Meets Lambda: Following One Request Through
Last Thursday, July 9, AWS launched Amazon API Gateway. You define an HTTP API in the console, point each method at a Lambda function, an HTTP endpoint or another AWS service, and API Gateway takes care of authorization, throttling, caching, logging and even generating client SDKs for JavaScript, iOS and Android. Pair it with Lambda and you get a public API with no servers of your own anywhere. The What’s New entry has the short version, and it’s live in US East (N. Virginia), US West (Oregon) and EU (Ireland).
The console makes the happy path look like a couple of clicks, which is great until something doesn’t behave. So this post follows a single request from the client to a Lambda function and back, as the launch documentation describes it, and then gets into permissions, API keys, throttling and the bill.
Resources, methods and the four boxes
An API in API Gateway is a tree of resources and methods. A resource is a path like /greeting, and a method is a resource plus an HTTP verb, so GET /greeting and POST /greeting are separate methods with separate settings. Open a method and the console shows its execution as four parts, and those four parts are the whole mental model.
The method request is the contract with the caller. It holds the authorization type (NONE or AWS_IAM), whether an API key is required, which query string parameters and headers the method accepts, and optional request models, which are JSON Schema descriptions of the body.
The integration request is what API Gateway sends to the backend. For Lambda you pick the Lambda Function integration type, choose a region and a function name, and optionally add mapping templates. Templates are written in Velocity Template Language with JSONPath expressions, and they turn whatever the caller sent into the event your function receives.
This is where the first surprise lives. With input passthrough, the JSON body of a POST or PUT becomes the event, but the developer guide says a GET hands your function an empty JSON object by default. To use a query string, you declare it on the method request, which the API reference says makes it available to the integration, and write an input mapping template that copies it into the event.
What Lambda gives you right now
Lambda currently runs two languages. Node.js (the 0.10 series) has been there from the start, and Java 8 arrived on June 15. Memory goes from 128 MB to 1536 MB in 64 MB steps, after a bump from 1 GB to 1.5 GB on June 14, and CPU scales in proportion to the memory you pick. The timeout defaults to 3 seconds and tops out at 60 seconds per request.
A Node.js handler takes event and context, and you finish by calling context.succeed(result), context.fail(error) or context.done(error, result). Here’s a tiny one that validates its input, written in plain ES5 because that’s what Node 0.10 understands:
exports.handler = function (event, context) {
var name = typeof event.name === 'string' ? event.name.trim() : '';
if (name.length === 0 || name.length > 64) {
context.fail(new Error('BadRequest: name must be 1 to 64 characters'));
return;
}
context.succeed({ greeting: 'Hello, ' + name });
};
Behind a POST method with passthrough, a body of {"name": "Adam"} becomes event.name. Behind a GET, event.name stays undefined until a mapping template puts something there, so this function would reject every GET until you write one.
Getting a real status code back
On the way out, the integration response decides what the caller sees. For a Lambda integration each integration response has a Lambda error regex, which the guide describes as matching the function’s error string, and each points at a status code you’ve declared on the method response. The method response is the other half of the contract with the caller: status codes, response headers and response models.
So to turn that BadRequest failure into a 400, you add a 400 to the method response, then add an integration response with a regex like BadRequest.* that maps to it, plus an output template if you want to reshape the error body. Starting every error message with a fixed prefix gives the regex something dependable to match. CORS headers are mapped the same way, and right now it’s manual: the guide has you create an OPTIONS method and map Access-Control-Allow-* headers yourself. Its recipe uses '*' for the allowed origin, which you’ll want to narrow for anything that isn’t public.
Deployments and stages
Editing a method changes nothing callers can see. You choose Deploy API, which creates a deployment, and put it in a stage. The stage name becomes the first segment of the path, so a stage called prod serves https://{api-id}.execute-api.{region}.amazonaws.com/prod/greeting. Each stage points at one deployment and has its own settings for caching, CloudWatch logs and metrics, and throttling, with per-method overrides. If a deploy goes wrong, Change Deployment points the stage back at an earlier one.
Launch limits are 60 APIs per account, 300 resources per API and 10 stages per API. Custom domain names are served through CloudFront with a certificate you supply, and every endpoint is HTTPS only.
Permissions go both ways
There are two separate IAM questions here. The first is whether API Gateway is allowed to invoke your function. When you save a Lambda integration, the console asks to give API Gateway permission to invoke the function. The Lambda docs describe that as a permission added to the function’s access policy, and invoke permissions are granted on the lambda:InvokeFunction action. The REST API reference also describes a credentials setting on integrations that can name an IAM role for API Gateway to assume instead. The second question is what the function itself can do, which comes from its execution role. The guide’s walkthrough role grants logs:* on every log resource, fine for a demo, but scope yours to what the function really touches.
Then there are callers. Setting a method to AWS_IAM means requests must be signed with AWS Signature Version 4. The generated SDKs do the signing, and Amazon Cognito can hand an app temporary credentials. If you use OAuth or your own tokens, the FAQ suggests leaving signing off and forwarding the token to your backend for verification, which with Lambda means mapping that header into the event and checking it in the function.
API keys and throttling
You create an API key, associate it with an API and stage, mark methods as requiring one, and callers send it in an x-api-key header. Both the launch post and the FAQ say keys are for metering third party usage, not for authorization. A key shipped inside a mobile app or a web page is readable by anyone who looks, so treat it as an identifier.
Throttling uses a rate and a burst limit per second, applied by token bucket at the stage and overridable per method. The FAQ’s example is 1,000 requests per second with bursts to 2,000, and anything over the limit gets a 429, which the generated SDKs retry. The important line in that FAQ is that by default API Gateway sets no cache and no throttling limits on your methods. An unauthenticated method with no throttle passes every request straight to Lambda.
Lambda has its own ceiling: a default of 100 concurrent requests per account. Because that limit covers the whole account, a flood against one public endpoint can crowd out everything else you run on Lambda. Set method throttles below what your account can absorb.
What it costs
API Gateway charges $3.50 per million calls plus data transfer out, starting at $0.09 per GB, with a free tier of one million calls a month for 12 months. Lambda charges $0.20 per million requests and $0.00001667 per GB-second, with duration rounded up to the nearest 100 ms, and its free tier of one million requests and 400,000 GB-seconds a month doesn’t expire.
Put those together for a small function and the gateway is the bigger line. A million calls to a 128 MB function that runs 200 ms costs about $0.62 in Lambda and $3.50 in API Gateway.
The stage cache is the other thing to watch, because it’s billed by the hour whether or not anyone calls. The smallest, 0.5 GB, is $0.020 an hour, about $14.40 for 30 days, and the largest, 237 GB, is $3.80 an hour. Cached responses are still billed as API calls, and when third parties call your API, you’re the one paying. Detailed metrics and logs are charged at CloudWatch rates, and the “Log full requests/responses” option writes full request and response data, so think about what’s in your payloads before turning it on. Keep Lambda timeouts close to what your function really needs, as the Lambda docs recommend, since execution time is what you pay for. One more trap: the console’s Test button really calls your method, so a test against a method that deletes things really deletes them.
Getting started
The API Gateway developer guide is the place to start. Its getting started section begins with a Lambda walkthrough that builds a GET and a POST method, deploys them to a test stage and calls them with curl. If you already describe your APIs in Swagger, AWS also published an import tool at launch.
My takeaway is that API Gateway is less magic than the console suggests, and that’s a good thing. It’s a pipeline you can reason about: method request, integration request, your function, integration response, method response, all frozen into a deployment behind a stage. Map exactly what your function needs, give failures real status codes, use IAM or real tokens instead of API keys for access control, and set a throttle before you share the URL.