A practical guide to serverless computing on AWS — how Lambda works, how to write and deploy functions, and how to connect them to the rest of your infrastructure.
The traditional way to run code on the internet involves renting a server, installing an operating system, configuring a runtime environment, deploying your application, and then keeping that server running around the clock whether it is handling requests or not. You pay for the server regardless of how much work it is actually doing, and you are responsible for patching it, monitoring it, and scaling it when traffic increases. Lambda is built on a completely different idea.
With Lambda, you write a function — a piece of code that does one thing — and upload it to AWS. From that point on, you do not think about servers at all. When something triggers your function, AWS spins up an execution environment, runs your code, and shuts everything down again. You are billed only for the time your code is actually running, measured in milliseconds. If your function receives no requests for an hour, you pay nothing for that hour. If it suddenly receives ten thousand requests in a minute, AWS handles the scaling automatically.
This model is called serverless, which is a slightly misleading name because there are obviously servers involved somewhere — you just do not manage them. What it really means is that the server infrastructure is completely abstracted away from you. Your job is to write code that works correctly; AWS's job is to run it reliably at whatever scale is needed.
Lambda functions are stateless by design. Each invocation is independent — one execution does not share memory or local variables with the next. If you need to persist data between invocations, you have to store it somewhere external, like DynamoDB or S3. This constraint is what makes Lambda scale so easily; AWS can run hundreds of copies of your function simultaneously without any of them interfering with each other.
Lambda supports multiple runtimes out of the box, including Python, Node.js, Java, Go, Ruby, and .NET. You can also bring your own runtime using a container image if none of the built-in options fit your needs. The choice of runtime is usually driven by what language you are already comfortable with or what libraries you need access to.
Every Lambda function has a handler — the entry point that AWS calls when the function is invoked. The handler receives two arguments: an event, which contains the data that triggered the invocation, and a context object, which provides information about the execution environment. Here is the simplest possible Lambda function written in Python:
That is genuinely all you need to get started. The function receives an event, ignores it, and returns a response with a status code and a body. If this function is connected to API Gateway, that response gets sent back to whoever made the HTTP request.
Deploying a Lambda function can be done in several ways. The simplest is through the AWS Console — you navigate to Lambda, create a new function, choose your runtime, and paste or upload your code directly in the browser. This works fine for small functions and quick experiments. For anything more serious, most people use the AWS CLI or a framework like the Serverless Framework or AWS SAM (Serverless Application Model), which let you define your functions in configuration files and deploy everything with a single command.
When your function needs dependencies — third-party libraries that are not included in the default Lambda runtime — you have to package them together with your code. In Python, this means installing your packages into the same directory as your function file and zipping everything together before uploading. Here is what that looks like for a function that uses the requests library:
The resulting zip file gets uploaded to Lambda and AWS extracts it into the execution environment when your function runs. For larger dependency sets, Lambda Layers are a cleaner alternative — you package your dependencies separately, upload them as a layer, and then attach that layer to one or more functions. This avoids duplicating the same libraries across multiple function packages.
Environment variables are the standard way to pass configuration to a Lambda function without hardcoding it. Things like API keys, database connection strings, and feature flags belong in environment variables rather than in your code. You set them in the Lambda console or in your deployment configuration, and read them in your code the same way you would in any other environment — in Python that means os.environ.get("MY_VARIABLE").
AWS Lambda getting started: https://docs.aws.amazon.com/lambda/latest/dg/getting-started.htmlA Lambda function sitting by itself does not do much. It needs something to trigger it — an event source that calls the function when something happens. AWS provides a large number of trigger options, and choosing the right one depends on what you are building.
API Gateway is the most common trigger for web-facing functions. When an HTTP request arrives at an API Gateway endpoint, it forwards the request to a Lambda function, waits for the response, and sends that response back to the caller. This is how you build serverless APIs and web backends. The event object your function receives contains the HTTP method, path, headers, query parameters, and request body — everything you need to handle the request.
S3 is another common trigger. You can configure a Lambda function to run automatically whenever a file is uploaded to a specific S3 bucket. This pattern is useful for image processing — generate a thumbnail every time a photo is uploaded — or for data pipelines where incoming files need to be parsed and stored somewhere else. DynamoDB Streams can also trigger Lambda, letting you react to changes in a database table in near real time.
Scheduled events are handled through EventBridge (formerly CloudWatch Events). You define a schedule using a cron expression or a rate expression, and Lambda runs your function on that schedule. This is how you implement things like nightly cleanup jobs, hourly data exports, or periodic checks against an external API.
For a Lambda function to call other AWS services — read from DynamoDB, write to S3, publish to SNS — it needs permission to do so. This is where execution roles come in. Every Lambda function is associated with an IAM role, and that role determines what the function is allowed to do. If your function tries to call a service it does not have permission for, the call will fail with an access denied error. Setting the execution role correctly, giving the function access to exactly the services it needs, is an important part of deploying Lambda responsibly.
Concurrency is something worth understanding before you go to production. By default, Lambda can run up to 1000 concurrent executions per region across all your functions. If your function gets a sudden spike in traffic and hits that limit, new invocations get throttled — they either wait or fail depending on how the trigger handles throttling. You can reserve concurrency for a specific function to guarantee it always has capacity, or set a maximum concurrency limit to prevent one function from consuming all available capacity in your account.
Lambda pricing has two components: the number of requests and the duration of each execution. As of the current pricing, you are charged $0.20 per million requests and $0.0000166667 per GB-second of execution time. A GB-second is one second of execution with 1 GB of memory allocated — if your function uses 512 MB and runs for 200 milliseconds, that is 0.1 GB-seconds. The free tier includes one million requests per month and 400,000 GB-seconds of compute time, and unlike some other free tier resources, these limits do not expire after twelve months. For most personal projects and small applications, Lambda costs effectively nothing.
The one performance characteristic of Lambda that surprises people is the cold start. When a Lambda function has not been invoked recently, AWS has to set up a fresh execution environment before running your code — load the runtime, initialise your code, and prepare everything. This initialisation step, called a cold start, adds latency to the first request after a period of inactivity. Subsequent requests hit a warm execution environment and are significantly faster.
Cold start duration varies by runtime and function size. Python and Node.js functions tend to have shorter cold starts than Java or .NET. Keeping your deployment package small helps, as does minimising the work done at the module level — code that runs when the function initialises rather than when it handles a request. For most applications the occasional cold start is acceptable. For latency-sensitive applications, Lambda offers Provisioned Concurrency, which keeps a specified number of execution environments warm at all times, eliminating cold starts at the cost of a small additional charge.
A few practical habits make Lambda easier to work with over time. Keep functions focused — a Lambda function that does one thing well is easier to test, debug, and maintain than one that tries to handle many different cases. Log generously; since CloudWatch captures your print statements and log calls automatically, there is no reason to be stingy with diagnostic output. Use environment variables for anything that might change between environments rather than hardcoding values. And always test your function locally before deploying — the AWS SAM CLI provides a local invocation tool that lets you simulate Lambda events on your own machine without incurring any AWS costs.
Lambda fits naturally into a broader serverless architecture alongside API Gateway, DynamoDB, S3, and EventBridge. The combination of these services lets you build applications that scale from zero to significant traffic without managing any infrastructure, and at a cost that is genuinely proportional to usage. Getting comfortable with Lambda is one of the most useful things you can do if you plan to build anything on AWS.