top of page
Logo [color] - single line w_ cart_edite

Evaluating Elixir + Phoenix

How Elixir can improve developer productivity for scalable and fault-tolerant web apps


Concurrency and fault-tolerance are two especially complex issues in Software Engineering, especially when your language of choice isn’t explicitly built for solving these issues. Elixir is a newer programming language, created in 2012 by José Valim, which is especially eye-catching in regards to the aforementioned issues.


Elixir is a spiritual successor to the Erlang programming language, as Elixir is built on top of Erlang’s VM, BEAM. Additionally, Elixir inherits Erlang’s OTP (Open Telecom Platform), which is used, according to the FAQ on the Erlang website, “to do everything from compiling ASN.1 to providing a WWW Server” (https://www.erlang.org/faq/introduction.html). Erlang/OTP, as the combination of the two is commonly referred to, was explicitly built for the purpose of building fault-tolerant applications and for “[finding] better ways of programming Telecom applications” (Armstrong 2003).

Erlang and Elixir are both functional languages, which are distinct from other languages in a few ways:

  • immutable variables (those which cannot be modified),

  • prioritizing pure functions (those that don’t have side effects), and

  • first-class functions, where functions can be returned, stored in variables, and passed as arguments.

In one way or another, these functional features are what allow Erlang, and by extension, Elixir, to have the properties that it does.


I had been hearing good things about Elixir for a while, including all the attributes mentioned above - fault tolerance, concurrency, and its functional style. I had also read about numerous positive experiences with Elixir’s poster child web framework, Phoenix.


The Phoenix framework is Elixir’s take on frameworks like Java Spring, Ruby on Rails, or Django. Built on top of Erlang’s web server, Cowboy, the framework was built as a tool to build websites that are both scalable and performant. Thus, the framework comes with many niceties, which make it easier to create modern, scalable, and performant web applications. LiveView is one such feature that really stuck out to me — the Phoenix Framework’s solution for creating modern, full-stack applications entirely in Elixir.


LiveView allows developers to write server-side rendered (SSR) web applications in Elixir, using WebSockets for communication between the frontend and backend. This leveraging of WebSockets makes LiveView an ideal tool for applications that need real-time updates on the frontend. Server-side rendering also brings with it the benefit (depending on your stance on JavaScript) of having to write significantly less JavaScript to make an interactive, stateful, modern application for the browser.


Regardless of your thoughts on JavaScript, there is certainly something to be said about having your frontend and backend unified — in one language and one codebase — reducing duplication of effort throughout, including validation, data models, and other common functionality. As well as code deduplication, server-side rendered websites allow the UI to be much more flexible, nimble, and efficient. When you change the data displayed on a page, for example, you do not have to modify the backend API at all, as you would in a client-side rendered solution. Instead, you just need to change what is rendered on the server, where you have all the data you need, including sensitive data, which is a whole can of worms on the frontend. There are a few drawbacks to this unification of the frontend and backend, for example, losing the ability to scale the infrastructure for the frontend and backend separately.


All of this is achieved by another feature of the Phoenix Framework: HEEx (HTML + Embedded Elixir), the Phoenix Framework’s templating language. Using HEEx, you can define the frontend for your application using standard HTML, CSS (or Tailwind, which is included by default - a nice touch), and JavaScript, while also allowing you to intermix Elixir within it. This is somewhat analogous to what JSX allows, in React, for instance. Phoenix will then take your HEEx, render it to HTML, once again, on the server, and send it to the client over WebSockets.


Phoenix does a ton of work here to optimize this, ensuring that only new data is sent to the client, but that’s too large a topic to talk about in detail here. These features, along with the plethora of features brought to the Phoenix framework either directly (such as built-in authentication and database connectivity using Ecto) or indirectly (through Elixir’s lineage to Erlang and the BEAM VM), enticed me to learn more about Elixir by creating a project in it.


The project I ended up building as a result of this inspiration, in order to dip my toes into the world of Elixir and Phoenix, is a Flight Tracker application, using the OpenSkyNetwork’s API, Google Maps and of course Phoenix with LiveView:

The app’s feature set itself is simple; you can control how often you poll for new flights, pause/unpause said polling, and move around the map to change your view. Zooming in on a specific area will cause flights only within said area to be updated to improve performance. Finally, you can hover over specific flights, which will display a box providing more information about the flight. This page, and the logic behind it, is written using a combination of Elixir with a small amount of vanilla JavaScript. JavaScript only handles the rendering of the Google Maps view, as well as handling updates to the map and its flights, sent from the Elixir / Phoenix program.


This brings me to another really cool feature I came across while developing this app: JavaScript interoperability. JavaScript interoperability, provided through a feature Phoenix LiveView calls “hooks”, enables integrating JavaScript into your Phoenix LiveView app, allowing communication between the JavaScript and the Elixir backend via message passing. For example, here is the whole hook that handles rendering the map, using Google’s maps JavaScript library, updating the flights as they are received from the backend, and informing the backend when the zoom of the map has changed.


The “hook.handleEvent” call on line 41 in the above snippet is an example of message passing between the client and server, in this case, server to client. On line 41, we see the client handling the event, with name “update_markers”, by calling the “updateMarkers” function and passing the payload of the event. This simple yet powerful method of communication keeps the JavaScript and Elixir pieces cleanly separate, while allowing them to work together. When using Elixir with Phoenix, you do not have to completely throw out the possibility of using JavaScript. Instead, we use it only when we need its benefits of expressing client-side interactivity.


The hook, as described above, is then integrated into our index page, written in Phoenix’s markup language HEEx.

This is, in fact, the whole page I showed earlier. If you are familiar with HTML, this should be pretty legible to you. Aside from a few changes, notably the HTML elements that begin with “.” and the weird-looking “<%= … %>” tags, this is mostly HTML with a few extra attributes in it. This is a huge boon for Phoenix in that any person who has used HTML and especially those who have used JSX, or the other templating languages, should be able to read and start writing HEEx relatively quickly.


In a little more depth, how is all of this interactivity being achieved with this minimal set of frontend code? First, let’s talk about those two oddities I mentioned, the tags that start with “.” and the “<%= … %>” tags, beginning with the former. The tags which begin with a dot (.form, .input, and .button) are simply Phoenix components, reusable functions which return HTML but are written in Elixir. The ones I have included are components that come built into Phoenix and mirror their HTML counterparts (i.e., those without the dot prefix). The commonality between all of the elements I included is that they involve user interaction. This helps elucidate why we need these custom components, they allow us to communicate with Phoenix’s backend. In both cases, that is for the range form and button, we define an attribute on each tag which begins with “phx-”, in this case “phx-change” and “phx-click”. These attributes represent events, change and click, respectively, with their values, “update_interval” and “toggle_pause”, as the messages we want to send to Phoenix when the event happens. These messages are additionally sent with the value of the input they represent, a number in the case of the range and a boolean in the case of the button.


The latter of the two things I wanted to talk about, the tags beginning and ending with “<%= … %>” are inline Elixir code. In the case of line 18, this is a simple way to switch the text of the button based upon whether the “paused” variable is true. As a side note, you may notice “paused” has the “@” symbol in front of it, this is how we let HEEx know that this is a variable passed in from the backend. These are used throughout the code, including in the tag’s attributes, surrounded by curly braces (“{}”).


The final code snippet I want to take a look at is the backend for the application. Specifically, I want to show off how the flights are retrieved and continuously polled for:

Here we define a method title “handle_info”. This method is used to handle messages sent from Elixir processes. In this case, we are handling the message titled “poll_flights”, which we can see as the first argument of the method. Specifically, the first argument is called an atom. In Elixir, atoms are named constants, whose values are their own name. Essentially, they take the place of passing in an arbitrary string. 


Going step by step, we first retrieve information about the app and then retrieve the bucket, which is a simple in-memory data store, from which we will obtain the current value for the interval. This is how long we wait between polling for new data. Next, as long as the app isn’t paused, we make a call to “Process.send_after”, in which we pass “:poll_flights” as the message to send, after a time equal to interval. This method sends a message after a specified amount of time. This is where continuous polling occurs. We recursively send the “:poll_flights” message from the handler that receives the message itself, so long as the app is not paused. I personally love the elegance of this solution, as it shows off the flexibility Elixir and Phoenix have built in. After setting up a new “:poll_flights” message to be sent, we retrieve the data for all the flights, as well as the current bounds for the map (in latitude and longitude). We retrieve the flights from an ETS table, another feature that comes built into Elixir. ETS is an in-memory data store that has been optimized and battle-tested to be reliable and fast; it was originally built for Erlang. Behind the scenes, we have another process that populates this table by making continuous api calls. Finally, we do a little bit of filtering of the data, removing the flights that are outside of the bounds of the map, and call “push_event”, passing the message “update_markers”, as well as the flights. This event is then handled by the JavaScript hook, defined above, and the flights are populated onto the map.


The other goal of this project was to find out how Elixir performed, especially in the case of concurrency. As a result, I performed a small amount of load testing on my app to demonstrate how it handles concurrency and scales as we add more cores or threads. To do so, I added a REST api to my backend, which allows a client to make a GET request to retrieve flights within boundaries passed in said request. I then used a very cool tool called Locust, which helps with load testing and visualizing stats about the tests. I ran the server on an EC2 instance of varying sizes and tested with a variable number of concurrent users. The results are as follows:


Instance Type c4-large (2 vCPU, 3.75 GiB Memory):

250 Concurrent Users


500 Concurrent Users


1000 Concurrent Users


Instance Type c4-xlarge (4 vCPU, 7.5 GiB Memory):

500 Concurrent Users


1000 Concurrent Users


2000 Concurrent Users


Instance Type c4-2xlarge (8 vCPU, 15 GiB Memory):

500 Concurrent Users


1000 Concurrent Users


2000 Concurrent Users


3000 Concurrent Users


Instance Type c4-4xlarge (16 vCPU, 30 GiB Memory):

500 Concurrent Users


1000 Concurrent Users


2000 Concurrent Users


3000 Concurrent Users


Examining the load testing results, I am impressed with Elixir’s ability to scale when provided with additional cores, threads, and memory. Upon each upgrade thrown at it, there was a significant gain in either requests per second or response times (usually both). In addition, the load that the server can take, without grinding to a halt (you can see this on the graph when the response time/requests per second crashes to zero), improved substantially as it went along.


I had a thoroughly great time experimenting with Phoenix / Elixir. I think this combination provides a great boon to developer experience and productivity, in my opinion. I think the fact that it scales so well vertically (not even mentioning the ability of Elixir to run in a distributed, multi-node fashion) is a fantastic benefit of Elixir as well. Obviously, you cannot expect the speeds of a low-level, high-performance language such as C, C++, or Rust, but that is not the point of Elixir and Phoenix. The point is to allow developers to create parallelizable, fault-tolerant web servers while providing a fantastic developer experience with numerous features included by default. In this goal, I believe Elixir excels.


References:


bottom of page