Session-lived Application Backends
• Paul Butler
The web browser has become the de facto delivery channel for new application software. Developers are targeting the browser for increasingly ambitious apps, from video editing to mapmaking and data tools.
It’s tempting to lump all browser-based software together as “web apps”, and people who build them as “web developers”. But the way developers are building desktop-like experiences in the browser has been diverging from the way that web-native applications (like blogs and social media sites) are built.
One difference that’s emerged is a pattern that we call session-lived application backends.
In a traditional web architecture, the backend layer consists of a pool of stateless, interchangeable servers. Every request sent from the frontend to the backend contains all the information needed to fulfill it, or at least, contains enough context for the server to locate that information in a database.
This approach is great for apps that are essentially just a database with a UI and some business logic, because state changes can be performed in isolation using only a tiny piece of the entire program state. For example, a calendar app does not need to read your entire calendar into memory to change the date of a meeting; it just sends an
UPDATE query to its database.
As more complex applications migrate to the browser, this approach breaks down. Instead of a calendar, consider a code editor (IDE). The program state of an IDE includes, in addition to your code, various data structures derived from that code to enable low-latency auto-complete and linting. These data structures are updated as frequently as every keystroke, and re-creating them from scratch each time would defeat their purpose. A stateless backend won’t do.
The options available to a developer at this point fall into three general approaches:
- Store all program state in a database or state cache, and reconstruct the last state at the beginning of every request. Taken to its extreme, this amounts to emulating an operating system’s context switch, in the application layer and over a network, and scales poorly as the size of program state grows.
- Move all the dynamic program state into the frontend. This is especially compelling now that WebAssembly exists. It also has limits, though. For one thing, the entire program state has to fit in the relatively modest memory the browser will allocate to it.
- Spin up a dedicated process on the server for each user, and maintain state in that process. Give the frontend a persistent connection to its dedicated process, and shut the process down when the user closes the app.
The third approach is what we call session-lived application backends. Of the three, it is the most difficult to shoehorn into a traditional web architecture, where statelessness is axiomatic. In spite of that, it’s quietly started popping up in applications that reach the limits of the other two approaches.
Examples of session-lived backends in the wild include:
- Developer tools that need to run a stateful language server, like Gitpod (as described in this talk) and GitHub Codespaces.
- Real-time multiplayer apps that manage non-trivial document state. Figma has described their approach of running a process for each active document to provide low-latency state synchronization.
- Pixel streaming servers like Stadia that use server-side compute to deliver high-performance graphics to modest end-user devices.
This looks like a set of disconnected niches, but we believe that as browser-based applications compete on features and performance, session-lived backends will become a building block for applications in every category that strive for a desktop-like experience.
Because session-lived application backends break the statelessness assumed by the traditional web stack, they’ve been out of reach to developers who don’t have a dedicated infrastructure team to architect and maintain them.
We’re trying to change that with the release of Spawner, our open-source (MIT-licensed) platform for session-lived backends.
With Spawner, you provide your backend as a container image (aka Docker image), and Spawner gives you a private HTTP API for “spawning” new instances of that backend in your cluster. The API returns a URI, which can then be used to open HTTP connections directly to that new backend, directly from your frontend code. Spawner monitors activity to the backend process, and shuts it down after a period of no connections. Here's a demo.
As a result of this architecture, Spawner is unopinionated about how you build your app, as long as it speaks HTTP (with support for WebSockets). It’s built on Kubernetes, which lets us stand on the shoulders of giants who have built, battle-tested, security hardened, and extended the popular open-source container orchestration framework.
Spawner is still under rapid development, but we’re eager to get feedback from developers pushing the envelope of browser-based applications.Spawner on GitHub