Skip to content

Serverless

Serverless isn't "no servers" — it's "you don't manage the servers , someone else does , and you pay only when your code runs" which sounds great until your bill arrives after a DDoS spike or you hit cold start latency at the worst possible moment AWS Lambda is the dominant serverless compute platform. You upload code , AWS runs it on demand , scales it to thousands of concurrent executions , and you pay per millisecond of execution time. No EC2 instances to patch , no SSH keys to manage , no capacity planning

AWS Lambda with Node.js

// index.js — Lambda handler
exports.handler = async (event, context) => {
  // event contains the trigger data (API Gateway request, SNS message, etc.)
  console.log('Event:', JSON.stringify(event, null, 2))

  // context has runtime info
  console.log('Remaining time:', context.getRemainingTimeInMillis())

  try {
    const result = await processRequest(event)

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(result)
    }
  } catch (err) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: err.message })
    }
  }
}

async function processRequest(event) {
  // Your business logic here
  const userId = event.pathParameters?.userId

  if (!userId) {
    throw new Error('Missing userId parameter')
  }

  // Database connections should be initialized outside the handler
  // for connection reuse across invocations with the same container
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId])

  return { user: user.rows[0] }
}

Serverless Framework — Infrastructure as Code

# Install Serverless Framework
npm install -g serverless

# Create a new service
serverless create --template aws-nodejs --path my-service
# serverless.yml
service: my-api

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  stage: ${opt:stage, 'dev'}

  # IAM permissions
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:GetItem
        - dynamodb:PutItem
      Resource: "arn:aws:dynamodb:${aws:region}:*:table/${self:service}-*"

  # Environment variables
  environment:
    NODE_ENV: ${self:provider.stage}
    TABLE_NAME: ${self:service}-${self:provider.stage}
    LOG_LEVEL: ${env:LOG_LEVEL, 'info'}

functions:
  createUser:
    handler: handlers/users.createUser
    events:
      - http:
          path: users
          method: post
          cors: true
          # Request validation
          request:
            schemas:
              application/json: ${file(schemas/create-user.json)}

  getUser:
    handler: handlers/users.getUser
    events:
      - http:
          path: users/{userId}
          method: get
          cors: true

  processPayment:
    handler: handlers/payments.processPayment
    events:
      - sqs:
          arn: arn:aws:sqs:us-east-1:123456789012:payment-queue
          batchSize: 10

  dailyReport:
    handler: handlers/reports.dailyReport
    events:
      - schedule: cron(0 6 * * ? *)     # Every day at 6AM UTC

plugins:
  - serverless-offline          # Local development
  - serverless-plugin-optimize  # Bundle and minify
  - serverless-prune-plugin     # Keep only last N versions

package:
  individually: true
  patterns:
    - '!**'
    - 'handlers/**'
    - 'node_modules/**'
    - '!node_modules/aws-sdk/**'     # AWS SDK is already in Lambda runtime

Deploy:

# Deploy to dev
serverless deploy

# Deploy to production
serverless deploy --stage production

# Invoke function locally
serverless invoke local --function getUser --path test-event.json

# View logs
serverless logs --function getUser --tail

Cold Starts — The Performance Killer

When a Lambda function hasn't been invoked recently , AWS provisions a new container and loads your code. That initial invocation is slower — sometimes 200ms to 2s slower depending on runtime and package size

Invocation 1 (cold start): 1200ms  ← Loading code , initializing runtime
Invocation 2 (warm):        45ms   ← Container reused
Invocation 3 (warm):        40ms   ← Still warm
... (container recycled after 15+ min of inactivity)
Invocation 4 (cold start): 1100ms  ← New container

Strategies to reduce cold starts: * Keep packages small — bundle with esbuild or webpack * Use provisioned concurrency (reserve warm containers — costs extra) * Initialize clients outside the handler (reuse DB connections , HTTP clients)

// BAD — client initialized inside handler (on every invocation)
exports.handler = async (event) => {
  const client = new DynamoDBClient({})   // Created every time
  // ...
}

// GOOD — client initialized outside handler (reused across invocations)
const client = new DynamoDBClient({})      // Created once per container

exports.handler = async (event) => {
  // client is already available , no initialization overhead
  // ...
}

// Also good — lazy initialization with singleton
let client
function getClient() {
  if (!client) {
    client = new DynamoDBClient({})
  }
  return client
}

Function Optimization — Every Millisecond Counts

Bundle size matters:

# Use esbuild to bundle Lambda functions
npm install esbuild

# serverless-esbuild plugin
plugins:
  - serverless-esbuild

custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: false
    target: node20
    packager: npm

Database connection management:

const { Pool } = require('pg')

// Connection pool persists across invocations
const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT, 10),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 5,
  idleTimeoutMillis: 60000,
  connectionTimeoutMillis: 5000
})

exports.handler = async (event) => {
  const client = await pool.connect()
  try {
    const result = await client.query('SELECT * FROM users')
    return { statusCode: 200, body: JSON.stringify(result.rows) }
  } finally {
    client.release()
  }
}

Timeouts and retries:

// Lambda timeout — max 15 minutes (900 seconds)
// API Gateway timeout — 29 seconds (can't exceed this for HTTP triggers)
// Set function timeout in serverless.yml:
functions:
  processPayment:
    handler: handlers/payments.processPayment
    timeout: 30       # Seconds
    memorySize: 512   # MB (more memory = more CPU)
    reservedConcurrency: 10  # Max concurrent executions

Event-Driven Patterns

Lambda excels at responding to AWS events — S3 uploads , DynamoDB streams , SNS notifications , SQS messages

S3 event (process uploaded file):

functions:
  processUpload:
    handler: handlers/processUpload.handler
    events:
      - s3:
          bucket: my-uploads-bucket
          event: s3:ObjectCreated:*
          rules:
            - prefix: uploads/
            - suffix: .csv

exports.handler = async (event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name
    const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '))

    console.log(`Processing file s3://${bucket}/${key}`)
    // Download , process , store results
  }
}

DynamoDB Stream (react to data changes):

functions:
  auditLog:
    handler: handlers/audit.handler
    events:
      - stream:
          type: dynamodb
          arn: arn:aws:dynamodb:us-east-1:123456789012:table/users/stream/2024-01-01
          batchSize: 100
          startingPosition: LATEST

Serverless Security — IAM and Function Permissions

The principle of least privilege is non-negotiable. Each function should have the bare minimum permissions to do its job

# BAD — too permissive
provider:
  iamRoleStatements:
    - Effect: Allow
      Action: dynamodb:*        # Full DynamoDB access — way too much
      Resource: "*"

# GOOD — scoped to specific table and actions
functions:
  createUser:
    handler: handlers/users.createUser
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:PutItem
          - dynamodb:GetItem
        Resource: arn:aws:dynamodb:${aws:region}:*:table/users-table

Function URL (public HTTP endpoint):

functions:
  publicApi:
    handler: handlers/api.handler
    events:
      - httpApi:
          method: ANY
          path: /{proxy+}
          auth:
            type: aws_iam     # Require IAM auth — or use JWT authorizer

Lambda environment variables encryption: Serverless Framework auto-encrypts environment variables using KMS


next → upcoming_05_message_queues.md