Serverless and Edge Runtime Part 2

Apiumhub - Oct 2 '23 - - Dev Community

This is the second post in a two-part series exploring the world of Serverless and Edge Runtime. In the previous post, we got familiar with serverless; the main focus of this post will be the Edge Runtime, where it can be useful, and what its caveats are.

Edge Location or Edge Runtime

While defining _what serverless is_ was quite straightforward, with Edge, we have a little more trouble. When ‘edge’ is mentioned, it can refer to a few, quite different concepts: it can refer to “edge location”, or “edge runtime”, or even “edge functions”. Let’s go through each, and see what exactly it means.

  1. Edge, the Location: the concept of running servers closer to our users. If we take AWS Lambda, we might have one instance deployed in the region us-east-2, meaning that’s where our server spins up and serves users. However, if we have a user from Asia, we can foresee that for him or her, just reaching our serverless function will have latency due to the geographical distance. We can deploy another serverless function in ap-east-1 (running the same exact code as in us-east-2) to serve users from the east, reducing the latency originating from the distance. Lambda@Edge from AWS fits into this category; we are essentially deploying “replicas” of our serverless function, and each user will be served by the instance that is geographically closest to them.

  2. Edge, the Runtime: unlike serverless functions, where an actual server has to spin up and then serve the request (thus, we bear the cost of the cold start), the edge runtime guarantees an environment where our code can execute immediately on a V8 platform without the need to start up any new servers. It is a technology that is designed to be integrated into frameworks, not directly into applications; probably most notably, NextJS uses it, but providers like Supabase have also built products on top of it. A massive caveat to note is that the Edge Runtime uses Javascript’s V8 engine under the hood, therefore, our application must be in Javascript or Typescript as well and not rely on any component that requires any kind of native runtime (C, C++, Rust, and so on).

  3. Edge Functions: these are code blocks, simple functions running on the Edge Runtime. While the Edge Runtime provides the platform for frameworks to use, Edge Functions are our small applications.

Global vs. Regional Edge

On paper, it might sound better if our servers/functions were as close to the users as possible, since surely, this would reduce the response time for the requests.

Right?

Most of the time, a point to consider here is where our database resides, geographically. Do we have a single Postgres instance running in the East US?

Since most of the time, our clients (hopefully) do not query the database directly, but go through a backend server, this can get quite important. If this backend is close to the database (in the same region), then the first request between the user and our backend might take a few milliseconds; however, if the endpoint the user hits up on the server does a number of roundtrips to the database (e.g., make a query first to see if the user is authorized, then query again to get some data, then query again to fetch something more, and so on), since the backend server is regionally close to the database, these n number of requests will not introduce much latency.

However, if we move our backend away from the database’s region, and place it as close to the user as possible, then the first initial request between the user and backend will be quicker, but then the server has to make longer roundtrips to the database. This can get out of hand quickly if our endpoint makes a considerable number of queries to the database. Vercel recommends regional Edge Functions in this case, which are deployed near the database dependencies. This means, our Edge Function might not be geographically closest to the user but to the database. Alternatively, we could even use globally distributed databases.

Vercel started to offer solutions for these as well, such as Postgres KV, Blob, and Edge Config.

By default, Edge Functions will run in every region globally, therefore, this regional way of working is something to opt-in, in to and configure ourselves for.

Turso

The company Turso offers an interesting solution to help with this potential issue as well: edge-hosted libSQL (SQLite) databases, which are deployed to Fly.io in 26 locations across the globe. We can strategically choose which locations to deploy to, aiming for them to be closest to our Edge Functions to reduce latency. It works by creating a primary instance of our database, which, once created, cannot be moved to a different location. Then, we can spin up replicas of it, which will get synchronized with the primary database. Read operations will be quick from these replicas, however, write operations will still need to go to the primary database.

CTA Software

Pros and Cons

With an understanding of what the Edge Runtime and Edge Functions are, we can evaluate some of their pros and cons.

Starting with the pros:

  • No cold starts whatsoever.
  • Close proximity: if we address the previously noted point about not placing our backend service far from our data stores, close proximity can still be achieved.
  • Cost: with serverless, we are paying for the computer we’ve used, but with Edge functions, we tend to pay per request. This makes it difficult to compare the cost, but anecdotally, for most use cases, Edge Functions tend to be cheaper, on average.

Now to consider some negatives:

  • V8 dependency: Javascript/Typescript apps can be executed only: Vercel mentions Node, and Supabase mentions Deno, as their runtime environments.
  • Maturity: the Edge Runtime and Functions are still considered to be bleeding edge technologies.

Migrating our JSON-CSV converter to an Edge Function

Similarly to the previous post, let’s try to get a sense of how to actually deploy an Edge Function. The process will feel extremely similar to how our serverless CSV converter was provisioned.

Since our JSON to CSV converter serverless function was written in Go, we cannot port it one-to-one to the Edge Runtime, on Vercel. We’ll have to convert our code either to TypeScript, or Javascript.

We can keep the same project structure with an “api” folder; however, inside it, we’ll create an index.ts file with the following example content from Vercel:

export const config = {
  runtime: "edge",
};

export default (request: Request) => {
  return new Response(`Hello, from ${request.url} I'm now an Edge Function!`);
};
Enter fullscreen mode Exit fullscreen mode

The ‘config’ object is necessary to indicate that this is in fact not a serverless function, but an Edge function; if we were writing a NextJS app, we’d have to use a similar notation as well.

Just like before, the CLI command of vercel deploy can be used to create a staging environment, and vercel --prod to push our API to a production environment.

Vercel deployment summary

Inspecting the build and deployment on Vercel’s UI, we can confirm that indeed, our code is deployed as an Edge Function, running globally (do you recall from earlier the difference between regional and global? for our CSV converter, since no database connection is necessary, opting to stay on the global deployment model would make sense)

Another point to note here is the Request and Response objects; they are standard Web API interfaces of the Fetch API.

Let’s now rewrite the Go code to TypeScript:

export const config = {
  runtime: "edge",
};

interface EmployeeData {
  name: string;
  age: number;
  jobTitle: string;
  badgeNumber: number;
}

export default async (request: Request) => {
  if (request.method !== "POST") {
    return new Response("Only POST requests are allowed.", { status: 405 });
  }

  const res = new Response();

  res.headers.append("Content-Type", "text/csv");
  res.headers.append("Content-Disposition", "attachment; filename=data.csv");

  const rows: string[][] = [];
  rows.push(["Name", "Age", "Job Title", "Badge Number"]);

  let employees: EmployeeData[] = [];

  try {
    employees = await request.json();
  } catch (error) {
    return new Response("Could not parse the JSON payload.", { status: 400 });
  }

  try {
    for (const employee of employees) {
      const row: string[] = [
        employee.name,
        employee.age.toString(),
        employee.jobTitle,
        employee.badgeNumber.toString(),
      ];
      rows.push(row);
    }

    const csvData = rows.map((row) => row.join(",")).join("\n");

    return new Response(csvData);
  } catch (error) {
    return new Response("Could not write CSV data.", { status: 500 });
  }
};
Enter fullscreen mode Exit fullscreen mode

After deploying it to Vercel, my URL this time is: https://vercel-ts-edge-example.vercel.app/

Just like in the previous post, if you decide to run the following curl, you will be able to convert employee data to a CSV, and this time, no cold starts should be experienced.

curl --location 'https://vercel-ts-edge-example.vercel.app/api' \
--header 'Content-Type: application/json' \
--data '[
    {
        "name": "John Doe",
        "age": 45,
        "jobTitle": "Software Developer",
        "badgeNumber": 58195
    },
    {
        "name": "Jane Doe",
        "age": 32,
        "jobTitle": "Software Developer",
        "badgeNumber": 58191
    }
]'
Enter fullscreen mode Exit fullscreen mode

References

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player