As part of a small web app project, I went through the process of deploying a site on Amazon Web Services using S3 + Cloudfront, and Route53 for the domain. Before writing up the web app itself, I learned a few new things from going through the deployment process from start to finish. It’s been four years since this site was first deployed to AWS; in that time I’ve kept up with some changes and missed out on others.

Previously, my go-to method would be to use the S3 website endpoint. Thanks to ✨ cloudfront functions ✨, some of the pitfalls of serving a site through the S3 REST API can be resolved, which makes it more viable as a hosting method.

While this kind of hosting setup is (conceptually) serverless, you can understand the difference between both methods in where the ‘server logic’ resides. If you use the S3 website endpoint, S3 will do things like set a default document for a directory (typically index dot html). If you use the S3 REST API, then S3 really does nothing more than serve up files, and anything beyond that has to be done in Cloudfront.

World Wide Web redirect

Redirecting a www-prefixed address to the bare domain (or vice versa) with S3 involved creating an empty bucket and setting it up to redirect all requests to your main bucket. For some reason this was always slow to redirect, and it felt like an inelegant workaround.

You can do the www redirect with the function in this Cloudformation script. I’ve attached this function to my ‘bare url’ cloudfront distribution:

function handler(event) {
  var response = {
    statusCode: 301,
    statusDescription: "Permanently moved",
    headers: {
      location: {
        value: "" + event.request.uri
  return response;

Default document url rewrite

Since the REST API will not do any redirects, it doesn’t know what to do if your URL points to a directory. We want it to return index dot html. The developer guide gives an example of how to do that.

function handler(event) {
    var request = event.request;
    var uri = request.uri;
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    return request;

After a few rounds of testing, each of these run within 10-20 units of compute utilisation, a percentage of the maximum time allowed before throttling. Currently the limit is 1ms, and the extra 0.2ms latency on every request is negligible.

These functions are nowhere near replicating the full feature set of a proper web server, but they’re enough for a static site.