|
| 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. |
0 commit comments