Skip to content

Modus-Logo-Long-BlackCreated with Sketch.

  • Services
  • Work
  • Blog
  • Resources

    OUR RESOURCES

    Innovation Podcast

    Explore transformative innovation with industry leaders.

    Guides & Playbooks

    Implement leading digital innovation with our strategic guides.

    Practical guide to building an effective AI strategy
  • Who we are

    Our story

    Learn about our values, vision, and commitment to client success.

    Open Source

    Discover how we contribute to and benefit from the global open source ecosystem.

    Careers

    Join our dynamic team and shape the future of digital transformation.

    How we built our unique culture
  • Let's talk
  • EN
  • FR

Serverless – AllTheThings (Part 2 of 3)

Published on January 7, 2020
Last Updated on April 8, 2021
Application Development, DevOps

This is part two of the Serverless-AllTheThings three part blog series. You can review the code from this series on the Serverless-AllTheThings repo.

In part 1 of this blog series, I covered what serverless is, what it isn’t and some of my serverless lovers spats over the years. In part 2 I’m digging into an example serverless front end and providing you with all the code you need to spin up an environment of your own. In part 3 I’ll explore an example serverless back end.

KISS

The most rudimentary way to host a serverless website on AWS would be to put all of the static files in an S3 bucket and configure the bucket to serve the files as a website. It satisfies the requirements to be serverless (i.e. AWS manages the servers and those servers scale to handle the demand) and is extremely cost effective. For many use cases, I would advise stopping here to relish the simplicity. See below for an example AWS CloudFormation S3 bucket template.

S3BucketStatic:
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"
      BucketName: "serverless-allthethings"
    Type: "AWS::S3::Bucket"

CloudFormation S3 Bucket
However, it’s not as fast (for the end user) as it could be, nor dynamic, nor sexy. So the next step would be to put a CloudFront distribution in front of the S3 bucket. This allows all of the S3 files to be served from a CDN of nearly 200 edge locations around the world. In layman’s terms, this means end users will be able to access your website files much faster. See below for an example AWS CloudFormation CloudFront distribution template.

CloudFrontDistribution:
    Properties:
      DistributionConfig:
        Comment: !Ref "AWS::StackName"
        DefaultCacheBehavior:
          AllowedMethods:
            - "GET"
            - "HEAD"
            - "OPTIONS"
          CachedMethods:
            - "GET"
            - "HEAD"
          Compress: false
          DefaultTTL: 31536000
          ForwardedValues:
            Headers:
              - "accept-encoding"
              - "x-uri"
            QueryString: false
          MaxTTL: 31536000
          MinTTL: 0
          SmoothStreaming: false
          TargetOriginId: !Join [
            "",
            [
              "s3:",
              !ImportValue "ServerlessAllTheThingsS3BucketStaticName",
            ],
          ]
          ViewerProtocolPolicy: "redirect-to-https"
        Enabled: true
        HttpVersion: "http2"
        IPV6Enabled: false
        Origins:
          - DomainName: !ImportValue "ServerlessAllTheThingsS3BucketStaticDomainName"
            Id: !Join [
              "",
              [
                "s3:",
                !ImportValue "ServerlessAllTheThingsS3BucketStaticName",
              ],
            ]
            OriginPath: !Sub "/${BranchSlug}"
            S3OriginConfig:
              OriginAccessIdentity:
                !Join [
                  "",
                  [
                    "origin-access-identity/cloudfront/",
                    !ImportValue "ServerlessAllTheThingsCloudFrontCloudFrontOriginAccessIdentityId",
                  ],
                ]
        PriceClass: "PriceClass_All"
    Type: "AWS::CloudFront::Distribution"

CloudFormation CloudFront Distribution
Well, that solved the speed issue, and it is a little sexier, but it’s still not dynamic. To solve this problem, I would point you to Lambda@Edge. Lambda@Edge is AWS’s serverless compute functionality (i.e. Lambda) that runs on the edge (i.e. CloudFront edge locations). CloudFront provides us with four places we can slip a Lambda@Edge function into its request/response flow (called event types):

  • viewer request
  • origin request
  • origin response
  • viewer response

This affords you with the opportunity to dynamically modify the request and/or response to your liking.

 

Request Flow Through CloudFront and Lambda@Edge Event Types

Livin’ Life On The Edge

To put things in CloudFront terminology, what we have so far is an S3 bucket origin server that contains all of our static files (js, css, images, etc.). What we want is to deliver a server-side rendered (SSR) single page app (SPA) via one of the Lambda@Edge viewer/origin request/response event types. All of the event types would work, but for a number of reasons (viewer request/response limits and sequential cold starts to name a couple) I have found it ideal to run SSR SPAs as an origin response. See below for an example AWS CloudFormation template with Lambda function and version resources and how they are associated with a CloudFront template.

LambdaFunctionApp:
    DependsOn:
      - "IamRoleLambdaApp"
    Properties:
      Code:
        S3Bucket: !ImportValue "ServerlessAllTheThingsS3BucketArtifactsName"
        S3Key: !Sub "${Commit}/app/lambda.zip"
      Description: !Sub "${AWS::StackName}-app"
      FunctionName: !Sub "${AWS::StackName}-app"
      Handler: "index.handler"
      MemorySize: 128
      Role: !GetAtt "IamRoleLambdaApp.Arn"
      Runtime: "nodejs8.10"
      Timeout: 20
    Type: "AWS::Lambda::Function"

LambdaVersionApp:
    DependsOn:
      - "LambdaFunctionApp"
    Properties:
      FunctionName: !Ref "LambdaFunctionApp"
    Type: "AWS::Lambda::Version"

CloudFrontDistribution:
    DependsOn:
      - "LambdaVersionApp"
    Properties:
        DefaultCacheBehavior:
          ...
          LambdaFunctionAssociations:
            - EventType: "origin-response"
              LambdaFunctionARN: !Ref "LambdaVersionApp"
    Type: "AWS::CloudFront::Distribution"

CloudFormation Lambda Function and Version and Relevant CloudFront Association
As for the other three Lambda@Edge event types, they can be used for a number of other purposes, including:

  • Compression handling
  • HTTP status code tweaking
  • URL redirects and rewrites

Putting It All Together

Let’s take a look at a few requests to see how they’re handled via the origin request and origin response Lambda@Edge functions in the Serverless-AllTheThings GitHub repo.

Static Request – #1

First up is a simple static request for a file with the URI /favicon.ico.

  1. CloudFront checks to see if it is cached (let’s assume it isn’t)
  2. The origin request Lambda@Edge function determines the ideal compression algorithm for static assets (let’s assume it is brotli) and updates the S3 file path
  3. S3 contains favicon.ico.br and responds with its contents and HTTP status code 200
  4. The origin response Lambda@Edge function passes it through
  5. CloudFront caches the response based on the accept-encoding header

Static Request – #2

Now that favicon.ico.br is cached in CloudFront, let’s look at another request with the same URI and accept-encoding header.

  1. CloudFront checks for and returns the cached contents of favicon.ico.br

Static Request – #3

Suppose there is a new request where the accept-encoding header prioritizes gzip. In this case, even if favicon.ico.br is already cached, the first request would follow static flow #1 above and return the contents of favicon.ico.gzip. Subsequent requests with the same gzip prioritized accept-encoding header would then follow static flow #2 above (where CloudFront would return the contents of favicon.ico.gzip).

Dynamic Request – #1

Now let’s take a look at a dynamic request for the URI / (i.e. the Home view).

  1. CloudFront checks to see if it is cached (let’s assume it isn’t)
  2. The origin request Lambda@Edge function determines the ideal compression algorithm for static assets and updates the S3 file path (let’s assume it is brotli)
  3. S3 does not contain a file and responds with HTTP status code 403
  4. The origin response Lambda@Edge function initiates the SSR process, renders the html for the Home view, determines the ideal compression algorithm for dynamic assets (let’s assume it is gzip), compresses the html, sets a short-lived cache-control timeout, and converts the HTTP status code to 200
  5. CloudFront caches the response based on the accept-encoding header

Dynamic Request – #2

Now that the Home view is cached in CloudFront, let’s look at another request with the same URI and accept-encoding header within the cache-control timeout.

  1. CloudFront checks for and returns the cached, rendered page

Dynamic Request – #3

Once the cache-control timeout has expired, the next request will follow dynamic request flow #1 above and then subsequent requests within the new cache-control timeout will follow dynamic request flow #2 above.

Dynamic Request – #4

Suppose there is a new request where the accept-encoding header prioritizes identity (i.e. no encoding). In this case, even if the Home view is already cached, the first request would follow dynamic request flow #1 above and return the rendered view without compression. Subsequent requests with the same identity-prioritized accept-encoding header would then follow dynamic request flow #2 above.

Instantaneous Scaling

In the above section I covered the basic requests under a minimum volume of users, but it doesn’t capture the true value that serverless brings to the table. Let’s suppose your website is featured in a prime position on Hackernews and a tsunami of users heads your way. How do you think the website will handle the flood of traffic?

Flood Scenario

Seconds after your website goes viral, let’s assume 1,000,000 new users simultaneously request your website home page (/) every minute (i.e. all one million requests happen on the first second each minute).

Minute 1

  1. One of the requests will follow the dynamic request flow #2 above. The other 999,999 requests will pause until the first request completes and then immediately respond with the cached content
    • This assumes all of the accept-encoding headers are the same. If not, there will be one request per unique accept-encoding header

Minute 2+

  1. All requests will receive the cached response until the cache-control timeout expires. At this point it will then follow the minute one flow above.

No requests are dropped and the handling of traffic instantly scales from zero to one million requests per minute. In non-Lambda environments, variations in traffic are traditionally handled by scaling a cluster of servers up and down, but it is far from instantaneous (i.e. during unexpected scale up events users will experience slower response times and/or requests will be dropped). Furthermore, while those servers are up, you are paying for them the whole time regardless of whether or not they are handling requests. Now what about the costs? Won’t one million serverless requests per minute be really, really expensive? Let’s do the math.

Cost

Lambda@Edge functions cost $0.60 per one million requests and $0.00000625125 for every 128MB-second used (metered at 50ms increments). In this scenario, let’s assume:

  • Origin request functions complete in under 50ms
  • Origin response functions complete in under 150ms
  • This flood of traffic occurs for 8 hours a day and there are zero requests in the other 16 hours each day
  • There are 200 unique requests every five minutes (due to accept-encoding headers, expired cache control and unique URIs)
  • Average transfer is 30KB

8 hours x 60 minutes x 1 million requests = 480 million requests
8 hours x 60 minutes / 5 minutes x 200 requests = 19,200 unique requests
480 million x (30/1024/1024)GB = 13.4 TB transferred

Lambda price = 0.0192 x $0.6 + 19,200 x (0.05 + 0.15)ms x $0.00000625125 = $0.01 + $0.02 = $0.03

In other words, the dynamic server cost (i.e. Lambda) for one flood day is about three cents. The supporting infrastructure (CloudFront and Route 53) is likely used in both serverless and non-serverless environments and is where the bulk of the costs will reside, but for completeness the math is below. In a worst case scenario where nothing is cached by users’ browsers:

CloudFront transfer price = $0.085/GB x 10TB + $0.08/GB x 3.4TB = $1,150
CloudFront request price = $0.1 x (480 million / 10,000) = $4,800
CloudFront total price = $5,950

Route 53 price = $0.4 * 480 = $192

Note: These are back-of-envelope calculations, actual costs will vary.

Performance

Serverless is faster than fast. Quicker than quick. It is Lightning! (Ka-Chow!)

For requests where no Lambda functions are warm nor has anything been cached by CloudFront (i.e. the worst case scenario), it takes about 1.61 seconds for the request to complete and the DOM content to be loaded.

For requests where Lambda functions are warm and everything has been cached by CloudFront, it takes about 119 milliseconds for the request to complete and the DOM content to be loaded.

In Summary

Serverless is awesome and is the perfect choice for both static and dynamic front ends. Stay tuned for part 3 of this serverless blog series where we’ll explore an example serverless back end and provide you with everything you need to spin up an environment of your own.

Posted in Application Development, DevOps
Share this

Lucas Still

Lucas Still is a software architect at Modus Create. As a serverless evangelist and full stack wizard, Lucas spends his days converting technical dreams into digital reality. He is a certified AWS solutions architect professional, Vue enthusiast, TypeScript fan and RDBMS buff. Lucas is a family man living in the state for lovers in USA. When not coding, you’ll likely find him playing with his kids, DIY'ing his house or traveling around the country.
Follow

Related Posts

  • Serverless All The Things
    Serverless - AllTheThings (Part 1 of 3)

    Serverless software architecture reduces maintenence and operation costs of computing, provides scalable, on demand data…

  • Promoting Blog Entries with AWS Lambda and Slack
    Promoting New Blog Entries with AWS Lambda and Slack

    Here at Modus Create, we're avid users of Slack as a team collaboration tool and…

Want more insights to fuel your innovation efforts?

Sign up to receive our monthly newsletter and exclusive content about digital transformation and product development.

What we do

Our services
AI and data
Product development
Design and UX
IT modernization
Platform and MLOps
Developer experience
Security

Our partners
Atlassian
AWS
GitHub
Other partners

Who we are

Our story
Careers
Open source

Our work

Our case studies

Our resources

Blog
Innovation podcast
Guides & playbooks

Connect with us

Get monthly insights on AI adoption

© 2025 Modus Create, LLC

Privacy PolicySitemap
Scroll To Top
  • Services
  • Work
  • Blog
  • Resources
    • Innovation Podcast
    • Guides & Playbooks
  • Who we are
    • Our story
    • Careers
  • Let’s talk
  • EN
  • FR