StackPractices
intermediate By Mathias Paulenko

Build Serverless Functions

Create and deploy serverless functions with AWS Lambda, Google Cloud Functions, and Azure Functions for event-driven, pay-per-use compute.

Topics: serverless

Overview

Serverless computing lets you run code without provisioning or managing servers. You write functions, upload them to a cloud provider, and the platform handles scaling, patching, and availability automatically. You pay only for execution time — making it ideal for sporadic workloads and event-driven architectures.

Below is a practical approach to creating and deploying serverless functions with AWS Lambda, Google Cloud Functions, and Azure Functions.

When to Use

Use this resource when:

  • You have event-driven workloads (webhooks, file processing, scheduled jobs). See Event-Driven Functions for event-driven patterns.
  • You want automatic scaling from zero to thousands of requests. See Cold Start Optimization for minimizing startup latency.
  • You need to avoid server maintenance and infrastructure overhead
  • Your traffic is sporadic and provisioning servers would be wasteful. See Serverless API Gateway for building pay-per-use APIs.

Solution

Python (AWS Lambda)

import json
import boto3

def handler(event, context):
    # Extract query parameter
    name = event.get("queryStringParameters", {}).get("name", "World")

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": f"Hello, {name}!"})
    }

JavaScript (Google Cloud Functions)

const functions = require('@google-cloud/functions-framework');

functions.http('helloHttp', (req, res) => {
  const name = req.query.name || 'World';
  res.json({ message: `Hello, ${name}!` });
});

Java (Azure Functions)

import com.microsoft.azure.functions.*;
import java.util.Optional;

public class HelloFunction {
    @FunctionName("hello")
    public HttpResponseMessage run(
        @HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS)
        HttpRequestMessage<Optional<String>> request,
        final ExecutionContext context) {

        String name = request.getQueryParameters().getOrDefault("name", "World");
        return request.createResponseBuilder(HttpStatus.OK)
            .body("Hello, " + name + "!")
            .build();
    }
}

Explanation

Serverless platforms abstract infrastructure management:

  • Event triggers: HTTP requests, file uploads, database changes, timers
  • Automatic scaling: Each invocation runs in a fresh container; platforms scale containers up and down
  • Pay-per-use: Billed by milliseconds of execution and number of invocations

Cold starts occur when a function hasn’t been invoked recently. The platform must initialize a new container, adding latency (100ms–3s depending on runtime and memory allocation).

Variants

PlatformRuntimeTrigger TypesCold Start
AWS LambdaPython, Node, Java, Go, RubyHTTP, S3, SNS, SQS, EventBridge, Cron100ms–1s
Cloud FunctionsNode, Python, Go, JavaHTTP, Pub/Sub, Storage, Firestore, Cron200ms–2s
Azure FunctionsNode, Python, Java, C#HTTP, Blob, Queue, Event Grid, Timer200ms–3s

What Works

  • Keep functions small and focused: One function per responsibility; compose complex workflows with step functions
  • Minimize deployment package size: Remove unnecessary dependencies to reduce cold start time
  • Use provisioned concurrency for latency-sensitive workloads: Pre-warm containers for critical paths
  • Store state externally: Functions are stateless; use DynamoDB, Redis, or Cloud Firestore for persistence
  • Set memory and timeout limits appropriately: Undersized memory causes OOM; oversized wastes money

Common Mistakes

  • Storing state in the function container: Local variables are lost between invocations; containers may be reused, but never depend on it
  • Ignoring cold starts: User-facing synchronous APIs suffer from cold start latency
  • Over-provisioning memory: Lambda allocates CPU proportionally to memory; find the sweet spot
  • Not handling partial failures: Batch processing must handle retries without duplicating work
  • Tight coupling to one vendor: Use abstraction layers (Serverless Framework, SAM) for portability

Advanced: Cold Start Mitigation

# AWS Lambda: initialize outside handler for connection reuse
import json
import boto3

# These run on cold start only — reused across invocations
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('users')
http_session = requests.Session()

def handler(event, context):
    # Handler stays thin — connections are already warm
    user_id = event['pathParameters']['id']
    response = table.get_item(Key={'id': user_id})
    return {
        'statusCode': 200,
        'body': json.dumps(response.get('Item', {}))
    }

Initialize database connections, HTTP clients, and heavy imports outside the handler function. The platform reuses the container across invocations, so these initializations run once on cold start and persist. Keep the handler body minimal — just process the event and return.

Advanced: Step Functions Orchestration

{
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:ValidateOrder",
      "Next": "CheckInventory"
    },
    "CheckInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:CheckInventory",
      "Next": "ProcessPayment"
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:ProcessPayment",
      "End": true
    }
  }
}

Step Functions chain Lambda invocations with built-in retries, error handling, and state management. Each step is independently retryable. Use Choice states for conditional branching, Parallel states for concurrent execution, and Map states for fan-out patterns.

Advanced: Provisioned Concurrency

# AWS CLI: configure provisioned concurrency
aws lambda put-provisioned-concurrency-config \
  --function-name my-api \
  --qualifier production \
  --provisioned-concurrent-executions 10

Provisioned concurrency keeps execution environments warm and ready to respond. Configure it for latency-sensitive endpoints where cold starts are unacceptable. Monitor utilization — if provisioned capacity sits idle, reduce it. For spiky traffic, combine provisioned concurrency with on-demand scaling.

Advanced: Event-Driven Architecture

import json
import boto3

s3 = boto3.client('s3')
sns = boto3.client('sns')

def handler(event, context):
    # Triggered by S3 upload
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']

        # Process uploaded file
        response = s3.get_object(Bucket=bucket, Key=key)
        content = response['Body'].read().decode('utf-8')

        # Publish result to SNS
        sns.publish(
            TopicArn='arn:aws:sns:us-east-1:123:processing-complete',
            Message=json.dumps({
                'file': key,
                'size': len(content),
                'lines': content.count('\n')
            })
        )

    return {'statusCode': 200, 'body': json.dumps({'processed': len(event['Records'])})}

Event-driven functions respond to cloud events: S3 uploads, DynamoDB streams, SQS messages, SNS notifications, or EventBridge schedules. Each event contains records that your handler processes independently. Design handlers to be idempotent because event sources may deliver duplicates. Use partial batch responses (Lambda) to report which records succeeded and which should be retried.

Advanced: Local Development

# template.yaml — AWS SAM local development
Resources:
  ProcessUpload:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.handler
      Runtime: python3.11
      Events:
        Upload:
          Type: S3
          Properties:
            Bucket: uploads
            Events: s3:ObjectCreated:*

# Run locally:
# sam local start-api
# sam local invoke ProcessUpload --event events/s3-upload.json
# sam local generate-event s3 --bucket uploads --key data.csv > events/s3-upload.json

AWS SAM CLI, Azure Functions Core Tools, and Google Cloud Functions Emulator provide local execution environments. Generate sample events with sam local generate-event to test handler logic without deploying. Use sam local start-api to test HTTP-triggered functions with curl or Postman. Always run a final integration test in the cloud — local emulators don’t replicate all platform behaviors.

When to Avoid

  • Long-running computations: Tasks exceeding 15 minutes (Lambda) need container-based compute (ECS, Cloud Run, EKS)
  • Stateful applications: Real-time games, WebSocket servers, or streaming services need persistent connections
  • High-frequency invocations: If your function runs 1000+ times per second, a always-on server may be cheaper
  • Complex dependencies: Large native libraries or custom runtimes increase cold start and deployment complexity

Troubleshooting

  • Cold start latency is high: increase provisioned concurrency, reduce package size, and avoid initializing heavy clients per invocation.
  • Function times out: check downstream dependencies, memory allocation, and retry logic. Increase timeout only after optimizing the code.
  • State lost between invocations: serverless functions are stateless. Persist state in a database, cache, or durable queue.
  • Deployment package too large: exclude dev dependencies and unused assets.
  • Event ordering issues: many event sources are at-least-once and unordered. Design for idempotency and explicit sequencing.

Further Reading

  • Official documentation: check the current reference for the framework or tool used.
  • Related guides: explore the serverless and lambda guides for deeper coverage.
  • Complementary patterns: review design patterns applicable to your technology stack.
  • Public postmortems: study real incidents from teams that faced similar production issues.

Production Notes

  • Deploy gradually using canary or blue-green to catch regressions early.
  • Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
  • Document the rollback in the runbook; test the procedure in staging at least once per quarter.
  • Review structured logs with correlation IDs to trace requests end-to-end during incidents.

Key Takeaways

  • Apply build serverless functions when you need a practical solution for your use case.
  • Monitor performance after implementation; measure latency, errors, and resource usage before and after.
  • Check the Troubleshooting section for common failures; most have documented root causes with fixes.
  • Keep dependencies updated and run tests in CI to prevent production regressions.

Common Production Pitfalls

  • Copying the example without adapting it to real data volumes and failure modes.
  • Skipping load and error-injection tests before the first production deployment.
  • Hard-coding values that should be configurable per environment.
  • Forgetting to add logging and monitoring at each step.
  • Deploying without a rollback plan or a tested backup strategy.
  • Assuming the minimal example will scale without adding caching or batching.
  • Not documenting the version and configuration used in production.
  • Letting the recipe sit unchanged when dependencies or scale evolve.

Frequently Asked Questions

How do I reduce cold start latency?

Use smaller runtimes (Node.js, Python) over Java. Reduce package size. Use provisioned concurrency. Keep connections (database, HTTP) warm between invocations by initializing outside the handler.

Can serverless functions handle long-running tasks?

AWS Lambda max is 15 minutes, Cloud Functions 60 minutes, Azure Functions configurable. For longer tasks, use step functions to orchestrate multiple short functions or move to container-based compute (ECS, Cloud Run).

How do I debug serverless functions locally?

AWS SAM CLI, Azure Functions Core Tools, and Functions Framework for Node.js all provide local emulators. Test locally, but always validate in the cloud since behavior can differ.