Skip to content

Commit f617233

Browse files
committed
Add post on serverless websockets
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <[email protected]>
1 parent bdfebce commit f617233

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed
+378
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
---
2+
title: "How to Integrate WebSockets with Serverless Functions and OpenFaaS"
3+
description: "We show you how to deploy an existing WebSocket server as a function, and how to modify an existing template to support WebSockets."
4+
date: 2025-02-27
5+
author_staff_member: alex
6+
categories:
7+
- websockets
8+
- sse
9+
- ai
10+
- agents
11+
- streaming
12+
dark_background: true
13+
image: /images/2025-02-ws/background.png
14+
hide_header_image: true
15+
---
16+
17+
We show you how to deploy an existing WebSocket server as a function, and how to modify an existing template to support WebSockets.
18+
19+
We'll also cover:
20+
21+
* How OpenFaaS can support WebSockets natively, when cloud-based solutions do not
22+
* Auto-scaling for WebSockets
23+
* A singleton approach for maintaining state for AI agents and chat applications
24+
* Extended timeouts to support long-lived WebSockets
25+
* Server Sent Events (SSE) as an alternative to WebSockets
26+
27+
When we talk about serverless functions, that typically means a short-lived, stateless piece of code that is triggered by an event. WebSockets take a different approach, and need to run for an extended period of time, and maintain a stateful connection to the client. Cloud-based functions offerings like AWS Lambda and Google Cloud Run tend to have very short timeouts, and make it difficult to maintain state. This is where a framework like OpenFaaS, which is built to run with containers, on infrastructure that you control, comes into its own.
28+
29+
[WebSockets](https://en.wikipedia.org/wiki/WebSocket) offer bidirectional streaming, which makes them ideal for chat interfaces, notifications, LLM agents, and other cases where you need to push data to the client.
30+
31+
WebSocket servers need to handle the various events that occur during a connection: *open*, *message*, *close*, and *error*. They also support broadcasting messages to all of the currently connected clients, or sending messages to a specific client.
32+
33+
An alternative to WebSockets is [Server Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events), which is a server push technology. It's what you use when you work with the [OpenAI](https://platform.openai.com/docs/overview) or [Ollama](https://github.com/ollama/ollama/blob/main/docs/api.md) REST APIs. An initial connection is made to the server, the client sends its request and then the server streams the response back to the client. This is a simpler approach, and is easier to implement in a serverless environment, and we added support for this in OpenFaaS in January 2024: [Stream OpenAI responses from functions using Server Sent Events](/blog/openai-streaming-responses/).
34+
35+
Server Sent Events fit the serverless paradigm well, and allow for many of the same use cases as WebSockets, so we'd recommend them as a first port of call.
36+
37+
That said, WebSockets now be used with [OpenFaaS Standard/Enterprise](https://openfaas.com/pricing/) and the [OpenFaaS Edge (faasd-pro)](https://docs.openfaas.com/deployment/edge/). We'll take a look at how in this post.
38+
39+
## Two options for WebSocket support in a function
40+
41+
There are two options for WebSocket support in a function: modify an existing template to handle WebSocket events such as *open*, *message*, *close*, and *error*, or package a HTTP server in a Dockerfile.
42+
43+
Option 1 is to pick one of the existing templates and to adapt its entrypoint and handler to handle WebSockets in the way you want.
44+
45+
Whilst we did add SSE support to our official templates, we did not do the same for WebSockets, because one size does not fit all.
46+
47+
Option 2 is that you can write your code in exactly the same way you would, any other application in your preferred language, with your preferred frameworks. You then package it into a container image using Docker, and deploy it via faas-cli, as if it were a function.
48+
49+
For Go, that's likely going to be [gorilla/websocket](https://github.com/gorilla/websocket), for Python that might be [Flask-SocketIO](https://flask-socketio.readthedocs.io/en/latest/), and for Node.js it's probably [ws](https://github.com/websockets/ws).
50+
51+
In order to deploy your code as a function, you'll just need to make sure its HTTP server binds to port 8080, and implements a health `/_/health` and a readiness `/_/ready` handler. It's OK if you only return a 200 from these endpoints whilst you're getting started. You'll also need to write a Dockerfile that builds and packages your application and then you can build/deploy it via the OpenFaaS CLI.
52+
53+
To test out the WebSocket support for existing applications, I tried packaging the server component of our [inlets](https://inlets.dev) product as a function. Inlets is used to expose HTTP or TCP services to the Internet over a WebSocket. I was able to deploy its container image via `faas-cli` and then connected a regular inlets-pro client to the function and accessed the exposed service.
54+
55+
So how do you decide which option to use?
56+
57+
The purpose of templates in OpenFaaS is to remove duplication and boilerplate between functions. For each new function, you pull down your custom template, and scaffold only the handler, and a way to provide dependencies.
58+
59+
If you end up feeling like the template approach doesn't fit your specific use case, then you can always package an application as a function with a Dockerfile.
60+
61+
## Option 1: Modify an existing template for WebSockets
62+
63+
I spent some time modifying the underlying index.js file to include similar code to our first example.
64+
65+
The changes the user will see involve the `handler.js` file, where we now export a `wsHandler` function, in addition to the existing `handler` function.
66+
67+
```js
68+
module.exports = {
69+
handler,
70+
wsHandler
71+
};
72+
```
73+
74+
This allows the index.js to send normal HTTP REST requests to one handler, and the WebSocket connections/events to another.
75+
76+
```js
77+
'use strict'
78+
79+
// handler handles a single HTTP request
80+
const handler = async (event, context) => {
81+
const result = {
82+
'body': JSON.stringify(event.body),
83+
'content-type': event.headers["content-type"]
84+
}
85+
86+
return context
87+
.status(200)
88+
.succeed(result)
89+
}
90+
91+
// WebSocketHandler responds to events from all connected
92+
// WebSocket connections
93+
class WebSocketHandler {
94+
constructor(server) {
95+
this.server = server;
96+
}
97+
98+
init(server) {
99+
this.server = server;
100+
}
101+
102+
handleConnection(ws, request) {
103+
console.log('[wsHandler] Client connected');
104+
}
105+
106+
handleMessage(message) {
107+
console.log('[wsHandler] Received:', message.toString());
108+
}
109+
110+
handleClose() {
111+
console.log('[wsHandler] Client disconnected');
112+
}
113+
114+
handleError(error) {
115+
console.error('[wsHandler] error:', error);
116+
}
117+
}
118+
119+
const wsHandler = new WebSocketHandler();
120+
121+
module.exports = {
122+
handler,
123+
wsHandler,
124+
}
125+
```
126+
127+
The WebSocketHandler class will handle the various events from the WebSocket connection.
128+
129+
Here's how you could respond to a message from a client:
130+
131+
```js
132+
handleMessage(message) {
133+
this.server.clients.forEach(client => {
134+
if (client.readyState === ws.OPEN) {
135+
client.send(message.toString());
136+
}
137+
};
138+
}
139+
```
140+
141+
To use this template, you'll need to pull down the template from my sample repository on GitHub:
142+
143+
```bash
144+
faas-cli template pull https://github.com/alexellis/node20-ws
145+
146+
faas-cli new --lang node20-ws ws1
147+
```
148+
149+
Then you can edit the `handler.js` file to add your custom logic.
150+
151+
If the style doesn't fit your needs, but you are sure that WebSockets are the right approach, then you can [fork the repository](https://github.com/alexellis/node20-ws) and modify the template to your needs.
152+
153+
## Option 2: Package existing code as a function with a Dockerfile
154+
155+
This example follows the first approach, which uses JavaScript/Node along with the express.js HTTP framework. WebSocket support is then added via the [ws](https://github.com/websockets/ws) library.
156+
157+
```bash
158+
mkdir -p ws/
159+
160+
faas-cli new --lang dockerfile fn1
161+
162+
cd ws/fn1
163+
npm init -y
164+
npm install express ws
165+
```
166+
167+
Delete the contents of the `./fn1/Dockerfile` file, and replace it with the following:
168+
169+
```dockerfile
170+
FROM --platform=${TARGETPLATFORM:-linux/amd64} node:20-alpine AS ship
171+
172+
ARG TARGETPLATFORM
173+
ARG BUILDPLATFORM
174+
175+
RUN apk --no-cache add curl ca-certificates \
176+
&& addgroup -S app && adduser -S -g app app
177+
178+
# Turn down the verbosity to default level.
179+
ENV NPM_CONFIG_LOGLEVEL warn
180+
181+
RUN chmod 777 /tmp
182+
183+
USER app
184+
185+
RUN mkdir -p /home/app
186+
187+
# Entrypoint
188+
WORKDIR /home/app
189+
COPY --chown=app:app package.json ./
190+
191+
RUN npm i
192+
193+
COPY --chown=app:app . .
194+
195+
# Run any tests that may be available
196+
RUN npm test
197+
198+
# Set correct permissions to use non root user
199+
WORKDIR /home/app/
200+
201+
CMD ["node /home/app/index.js"]
202+
```
203+
204+
Create an index.js file:
205+
206+
```js
207+
const express = require('express')
208+
const app = express()
209+
const ws = require('ws');
210+
211+
const okHandler = (req, res) => {
212+
res.status(200).send()
213+
};
214+
215+
app.use("/_/health", okHandler);
216+
app.use("/_/ready", okHandler);
217+
218+
const wsServer = new ws.Server({ noServer: true });
219+
220+
wsServer.on('connection', function connection(ws) {
221+
ws.on('error', error => {
222+
console.error('WebSocket error:', error);
223+
})
224+
225+
ws.on('message', function incoming(message) {
226+
console.log('received: %s', message);
227+
ws.send(`echo ${message}`);
228+
});
229+
230+
});
231+
232+
const server = app.listen(8080);
233+
234+
server.on('upgrade', function upgrade(request, socket, head) {
235+
wsServer.handleUpgrade(request, socket, head, function done(ws) {
236+
wsServer.emit('connection', ws, request);
237+
});
238+
});
239+
```
240+
241+
Now deploy the function to OpenFaaS:
242+
243+
```bash
244+
faas-cli up -f fn1.yml --tag=digest
245+
```
246+
247+
You should really update the image tag inside of `stack.yml` every time you change it i.e. `0.0.1` to `0.2.0` and so forth. For convenience, the `--tag=digest` flag will generate a new tag based upon the contents of the handler folder, and saves some typing during development.
248+
249+
You can now connect your WebSocket client to the `fn1` function using the gateway's URL:
250+
251+
When using TLS:
252+
253+
```
254+
wss://openfaas.example.com/function/fn1
255+
```
256+
257+
When using plain HTTP, i.e. on 127.0.0.1:
258+
259+
```
260+
ws://127.0.0.1:8080/function/fn1
261+
```
262+
263+
The following client can be used to test the function:
264+
265+
```js
266+
const ws = require('ws');
267+
const client = new ws('ws://127.0.0.1:8080/function/fn1');
268+
client.on('open', () => {
269+
270+
client.on('message', (data) => {
271+
console.log(`Got message ${data.toString()}`);
272+
});
273+
274+
client.on('close', () => {
275+
console.log('Connection closed');
276+
});
277+
278+
let n = 0;
279+
280+
let i = setInterval(() => {
281+
console.log('Sending message');
282+
client.send(`Hello ${n++}`);
283+
}, 1000);
284+
285+
setTimeout(() => {
286+
clearInterval(i);
287+
client.close();
288+
}, 10000);
289+
});
290+
```
291+
292+
## Timeouts for WebSockets
293+
294+
The default timeout for a function and the installation of OpenFaaS can be extended to very long periods of time. Whilst there is no specific limit, we'd encourage you to try to right-size the timeout to your typical needs, so that might mean setting it to 1 hour, instead of 24 hours. Browser-based clients can also be configured to reconnect.
295+
296+
If you're using your own code, then you just need to configure the Helm chart with a longer timeout.
297+
298+
If you're using one of our templates, with the of-watchdog, then you'll also need to timeouts for the function via environment variables.
299+
300+
You can learn more in the docs: [Extended timeouts](https://docs.openfaas.com/tutorials/expanded-timeouts/)
301+
302+
## Scaling WebSockets
303+
304+
Functions which expose WebSockets can be scaled horizontally by adding in extra replicas, or scaled to zero if there are no connections.
305+
306+
You can also force a function to act like a singleton, if you want to make sure it has the same state between multiple connections. If you were implementing a chat application or an AI agent, you may want to have one individual function deployment per customer, to maintain state. Idle replicas can be scaled to zero to save on resources.
307+
308+
The best scaling mechanism for WebSockets is the `capacity` type which works on the amount of TCP connections running against all the replicas of a function.
309+
310+
```yaml
311+
functions:
312+
fn1:
313+
labels:
314+
com.openfaas.scale.min: 1
315+
com.openfaas.scale.max: 10
316+
com.openfaas.scale.type: capacity
317+
com.openfaas.scale.target: 10
318+
```
319+
320+
The above rules will create a function with a minimum of 1 replica, a maximum of 10 replicas, and a target of 10 connections per replica.
321+
322+
The value of `com.openfaas.scale.target` is a target number, replicas may end up with slightly more or less than this
323+
324+
For hard-concurrency limits use the `max_inflight` environment variable, and make sure your code uses the OpenFaaS of-watchdog, which implements the limiting.
325+
326+
```yaml
327+
functions:
328+
fn1:
329+
environment:
330+
max_inflight: 10
331+
```
332+
333+
When using `max_inflight`, replicas of a function with at least 10 ongoing connections will be taken out of the load balancer's pool, and if they do receive a request will respond with a 429 "Too many requests" error. If you use this option, configure your client to retry requests until it can connect successfully.
334+
335+
If you want to create a singleton, you can override scaling to that there is only ever one replica of the function.
336+
337+
```yaml
338+
functions:
339+
fn1:
340+
labels:
341+
com.openfaas.scale.min: 1
342+
com.openfaas.scale.max: 1
343+
```
344+
345+
Scale to zero is also supported with WebSockets:
346+
347+
```yaml
348+
functions:
349+
fn1:
350+
labels:
351+
com.openfaas.scale.zero: true
352+
com.openfaas.scale.zero-duration: 15m
353+
```
354+
355+
The above will scale any functions to zero if they haven't had a new connection established within the last 15 minutes.
356+
357+
Learn more:
358+
359+
* [Autoscaling functions](https://docs.openfaas.com/architecture/autoscaling/)
360+
* [Scale to zero](https://docs.openfaas.com/openfaas-pro/scale-to-zero/)
361+
362+
## Conclusion
363+
364+
We covered two approaches for integrating with WebSockets. In the first approach, we created a new template called `node20-ws` based upon an existing one. We added support for the Node.js ws library through functions in the handler for the lifecycle events of a WebSocket connection. That custom template could be shared with your team very easily by pushing it to a public or private git repository. In the second example, we packaged existing code as a function with a Dockerfile, which gave us more flexibility, but at the cost of having duplication between each function.
365+
366+
In both cases, a standard client was used to connect to the function, and messages were echoed back and forth between the client and the function.
367+
368+
We then touched on how to scale WebSockets, and how to configure timeouts for functions.
369+
370+
But why isn't there "WebSocket support" in every official OpenFaaS template?
371+
372+
1. Server Sent Events (SSE) is a simpler, and more compatible, approach to streaming data to the client.
373+
2. WebSockets are complex, and used in many different ways, we couldn't build a template that suited every developer's needs.
374+
3. We'd rather have a small number of templates that are well-supported, and have a near-perfect developer experience.
375+
376+
We instead have provided a starting point where you can write your applications as if they were just being deployed through Docker, and an example of how to modify the template to support WebSockets.
377+
378+
If you'd like to try out websockets in OpenFaaS, feel free to [get in touch](https://forms.gle/g6oKLTG29mDTSk5k9) or join our [Weekly Community Call](https://docs.openfaas.com/community) to see a live demo.

images/2025-02-ws/background.png

48.3 KB
Loading

0 commit comments

Comments
 (0)