YTtoTranscriptTranscribe
YouTube Transcript

Distributed WebSockets in NestJS with Redis Streams

Michael Guay · 8,389 words · 42 min read · EN-ORIG

Distributed WebSockets in NestJS with Redis Streams
Watch on YouTube

Below is the complete, readable transcript of Distributed WebSockets in NestJS with Redis Streams by Michael Guay on YouTube. Read the full text, copy any part you need, or generate a transcript for any video with our free tool.

00:00

Hey everyone. Today, I'm super excited to bring you an advanced lecture on production-grade WebSocket architectures. Now, in this lecture, we're going to start off by building a standard WebSocket server inside of NestJS. And we'll also wire in a Socket.IO client here that will establish a connection to this WebSocket server to listen for new

00:22

messages emitted on that connection and display it, as well as being able to send new messages on that WebSocket connection. And so, I have two instances of my client here at localhost:3001. When we send a message in one of these clients, we can see it in the other. This is a standard WebSocket

00:40

architecture. Now, what we've done in this lecture as well is we've actually spawned a second instance of our WebSocket server. Because, for example, in production, we're running Node.js, we know we need to scale our application horizontally and add more instances. Well, what happens when we do this with a standard in-memory WebSocket architecture, we run into an issue. Now,

01:05

we'll walk through why we actually run into this problem shortly. However, I want to show you our solution to it, and that's going to be using Redis streams. So, in this case, I've actually launched a separate NestJS WebSocket server that's running in the background at localhost:3002. However, both of these instances are now

01:26

connected to this Redis stream so that even when we send messages in this client here, which is only connected to a separate WebSocket server, we see the same messages on the separate WebSocket server and client. And this is the amazing benefit that Redis streams gives us by introducing an append-only log that we can essentially subscribe to a

01:49

stream, that we can listen to a central store where we send and receive messages through the WebSocket.IO. Another amazing benefit to this is that we can also drop connections to the WebSocket server in a client, send more messages, and then when we reconnect to that same server, we get replayed any messages that we missed, ensuring we

02:12

have full data integrity and fault tolerance with our WebSocket architecture. So, I'm super excited to jump into all of this, start building it, and showing you how to implement a truly production-grade WebSocket architecture with Redis Streams that allows us to scale our WebSocket servers horizontally, as well as introduce some fault tolerance. So, let's go ahead and

02:33

jump right in. I'll see you there. Now, before we jump into the lecture, if you like content similar to this, head over to my developer membership website at michaelgway.dev. I'll leave a link below. Here, you can get access to hundreds of hours of exclusive developer content going over advanced tutorials inside of NestJS and

02:54

React, as well as get access to my best-selling courses here, including my latest one where we're building a clean architecture using domain-driven design. This course has over 9 hours of content and growing, and it really shows you how to build a truly clean architecture and the benefits this bestows upon your applications. So again, thanks so much for watching. I

03:18

hope you can check out my site because I think there's a lot of valuable content for you here. I'll leave a link below. Let's go ahead and jump right in. All right. So, before we jump into the actual solution, I want to describe the problem that we face with the typical in-memory WebSocket server here. And

03:38

let's go ahead and jump into this image that we have here. So, typically, what we have is, of course, the user making a request, and they hit a load balancer in front of our running instances. And this is because we have multiple instances of our application running in production, we want to scale horizontally. Now, the

03:56

reason why we run into an issue here is explained below because, for example, in this first instance of our application where we have our WebSocket server, let's say we have two clients that have established a connection. However, we have two more instances of our app running that have totally separate clients connected, and they know nothing about

04:17

these other clients connected to a totally different WebSocket server. So, what happens is in instance A, when we emit a WebSocket event to all connected clients of our application, that only includes these two clients that have connected and not any other clients that have connected to the WebSocket server through other instances that we've

04:38

scaled horizontally. And that's the crux of the issue. Our in-memory broadcast, by default, will not cross multiple instances and alert these other clients. Now, the solution to the problem that we're going to implement here is Redis streams. In Redis streams, we can see here acts as almost a centralized data store. Really, it's this append-only log

04:59

that allows us to open a stream up to where we can read and write to. And the beauty of this is now we have a centralized store that we can go to to publish and consume our WebSocket messages. And this allows us to share state between the multiple instances. So, we can see here on our first

05:17

instance of our application, if we were to, for example, emit an event over the WebSocket connection, well, we go ahead and alert all the clients that are connected. That's easy enough and the standard functionality. But now what happens is we use a special XADD publish event to Redis stream. However, this Redis stream is also

05:40

connected to all other instances. And these instances here are constantly pulling the Redis stream for more data, checking, do I have another message? Do I have another message? And so on. And when it detects this message from instance A that got emitted, it will then publish that same message to all connected clients to its web socket

06:01

connection. And so in this way, we have Redis streams fan out where every instance gets every message because of this centralized Redis stream data store. And that's what we're going to be building in this lecture. I'm so excited to jump in. Now we know the architecture. I'll see you there. All right. So let's go ahead and get

06:21

started with creating our new NestJS application where we'll add our Redis web socket server. So for this, I'm going to use the NestJS CLI to initialize a new project. If you don't have the CLI available, you can install it with PNPM or whatever package manager you're using. And you can install at NestJS/CLI

06:43

@latest. I'll go ahead and then use PNPM DLX @nestjs/cli. Use the new command and I'll call this NestJS Redis streams. This will go ahead and initialize the new NestJS application. Go ahead and let it complete. We'll choose our package manager, in this case PNPM, and allow it to install the dependencies. So now we can CD into our Nest

07:11

application and I'll open it up inside my code editor. And now we have the NestJS starting project ready to go. Let's go ahead and now add the dependencies that we're going to need to set up our Redis web socket server. So go ahead and run PNPM install. We're going to go ahead and add

07:33

@nestjs/platform-socket io, which is going to be a NestJS adapter to actually create the web socket server in NestJS decorators. We'll then go ahead and also install at NestJS platform /websockets which will give us access to web socket related library code. And then lastly we also then want socket .io/redis-streams-adapter. Now this library is going to allow us to

08:08

actually integrate Redis streams inside of our NestJS web socket gateway very easily without us having to manage any of the low-level code. It'll just plug and play out of the box which makes it very easy to get up and running. I'll also install ioredis which is the client library we use to actually

08:26

establish the underlying Redis connection. And lastly we'll add socket.io dependency directly which is the underlying server implementation itself for the web socket server. So go ahead and install all of these dependencies to make sure that we have them available in the application. So now we have the dependencies we need which is excellent. So next up we need

08:48

to make sure we have an instance of Redis itself running on our machine. So in my case I'm going to use Docker to run Redis locally. You can use whatever method you prefer if you want to install and run Redis natively on your machine or use Docker. If you want to use Docker like me just

09:04

make sure you download and install Docker Desktop which will include Docker Compose and allow us to create new Docker containers using Docker Compose files and that's what we use to run and manage Redis. So I'm going to create a new docker-compose.yaml at the root of the project and then paste in a pre-built Redis image

09:25

configuration. So this is the Docker Compose which defines the services we're going to run. In this case a new Redis service that uses the Redis image 7-alpine. I've also set some other properties here like the container name, the port that it runs on. We also have a command to start up the Redis server, do a simple

09:45

health check, and finally actually save and persist the data that we write to Redis on a volume locally. And that's what we've defined here so that even when the container restarts, our data will be persisted. And this is especially useful in Redis streams because as we know, a Redis stream is a written log of all events that get

10:06

posted. So, if for example our container goes down and we want to restart it, well, we want to maintain all of that data on the log so that our instances when they subscribe to this log through a stream, they'll get access to the latest data. So, with all of this in place, we can

10:25

now actually start up this Redis using Docker Compose. So, in a terminal, with Docker installed, I'll run Docker Compose up, which will go ahead and pull this Redis image and automatically start it. And now we can see here we have the actual container started up. So, our host port will be 6300 and the Redis

10:46

port is 6379. And we can see that here Redis is ready to accept TCP connections on port 6379. So, now our application is ready to connect to the Redis server. Let's go ahead and get started with developing our server to establish this connection. All right, so I've opened up another terminal inside of our working

11:07

directory, and I'm going to run the PNPM start dev command, which starts up our NestJS development server by compiling the application, starting it, and will automatically listen for changes as we develop. So, we can see here the Nest application is up and running. And by default, we know if we look in our main.ts where the application is

11:28

bootstrapped, we're opening up an HTTP server here on port 3000, that's what we're doing when we're calling listen. We have a root app module that declares a default app controller, and this exposes a single get route with the get hello method on the app service injected, which returns back a simple stub string.

11:51

So, with the server running, we should be able to go to localhost:3000 in browser or Postman and see the hello world response coming back here. So, this means our application is up and running. We're ready and listening for changes. We can go ahead and clean this up by removing our app files, the ones that

12:10

are default generated here. We are not going to use these, so I'll go ahead and remove them and also clean them up from the app module, just so that we're starting from a blank slate. All right, so the first thing that I want to do is set up our web socket gateway. Now, a gateway in NestJS is

12:28

simply the abstraction around a web socket server that allows us to easily set up a server that can listen for incoming web socket connections and then emit messages on those web socket connections programmatically. So, in order to create this gateway, let's go ahead and do this by starting off a new web socket

12:50

directory inside of source. And in here, I'll create this new notifications. gateway.ts file. Here, I'll use the web socket gateway decorator from NestJS web sockets. And this decorator is what NestJS will actually scan to determine that this is a web socket server. So, we can see we can also pass in a custom port if we

13:13

want the web socket server to listen on a different port than the default HTTP port. By default, the web socket gateway and all web socket servers in NestJS will bind to the same exact port that the server is already listening on and reuse this HTTP port that's passed into app.listen because WebSockets is simply built on

13:34

top of HTTP. It still can use the same server and it will actually just listen for requests at {slash} socket.io which the client libraries will use to actually establish and upgrade this WebSocket connection. So, it's important to point that out that this is still listening on the same default port by default. We can also pass in a full options

13:58

object if you wanted to set custom options like cores to set the allowed origins. But, in our case, we're going to run our client and server on the same host and port, so no need for any additional configuration. Let's go ahead and export the notifications gateway now. So, this will go ahead and implement the

14:17

on gateway connection. So, NestJS WebSockets, similar to other application life cycle hooks in NestJS, it gives us access to the points at which a connection lives through. So, when the gateway first initializes, the server starts, when the gateway is disconnecting, or when the gateway has a new connection, it will call certain methods that we

14:41

implement here. So, in our case, we want to run some code every time we establish a new WebSocket connection, and that's what on gateway connection will allow us to do. So, if we start typing, we can see the handle connection method that NestJS will call. Now, this client object here is can use the actual socket type from

15:01

socket.io. That's the underlying server that we're using here. And so, I'm going to import type server and socket from socket.io because we're going to use both of these as types. So, now we have access to the client connection object that is being established here in this method whenever it's called. Now, instead of handle

15:24

connection, I just want to log a statement out so that we can see that the connection was opened. So, I'm going to go ahead and log client a template literal and say client with the client object. This client object will have an ID to uniquely identify the connection object. And we'll just say it has connected. So,

15:45

this will tell us when a new connection has been established on the server. So, this is great. We can now listen for new connections, but we also want the ability to send messages on this WebSocket server because as we know, that's the whole benefit of WebSockets is we maintain this persistent HTTP connection that allows us to push

16:06

new messages to all connected clients. So, for this, I want to create a new broadcast method that can take in a notification message. And I'll define this notification type here. This is just a dummy notification object that will emit from our app. So, let's say that it has an ID, title, body, and timestamp.

16:33

So, this is to allow us to actually send some data that we'll include on our messages. And so, this notification now we'll say is of type notification. And so now, inside of broadcast, we want to then send this notification to all of the connected clients that have connected to this WebSocket server. Now, in order to do this, we can

16:54

actually get access to the underlying WebSocket server that NestJS opens up with Socket.IO here. So, we can mark this with the server type that I set up earlier from Socket.IO. This is the underlying server itself. In order to populate this, we use the @WebSocketServer decorator from this with the underlying server connection. Now, this allows us to get access to the

17:22

server where we can get access to methods on it. So, server has the method emit, which will automatically broadcast this message to every connected client that has been connected to this WebSocket server. So, now I'll just simply pass in the event name that we can then listen for on the client UI, as well as the object itself, which will

17:47

be the data that we send over the connection. Now, Socket.IO allows you to also send messages to only specific connections, which are known as rooms. However, in our example, we're going to broadcast this message to all of our connected clients because it's really the example that we're trying to show in this application where, as we'll see

18:06

later on, with this implementation, we are emitting the event to all connected clients in this instance of the server that's running. However, imagine if we have now multiple instances of this WebSocket server and we emit this event on just this instance of the app, well, this is only going to send the notification to any of

18:28

the connected clients on this instance of the Nest server. The other instances of our WebSocket server do not know about those clients, and that's exactly the problem we're going to see later on when we wire up the client, where if we have multiple instances, we will not get these events shared between them, and that's what we're

18:46

going to be solving with the Redis Streams adapter shortly. Let's go ahead and finish up our implementation here. So, we have the WebSocket gateway all wired up, which is excellent. We now need to add it to our NestJS app by going to the app module and adding it as a provider to the providers array so

19:05

that NestJS will actually scan the decorator and create the server. So, now we have the WebSocket server available, which is excellent. Next thing I want to do is expose a notifications controller and service that will actually be able to use the gateway to emit events and call that broadcast method, which will then

19:27

use from the client side. So, let's go ahead and wire this in next. So, to wire this up, I'm going to create a another directory called notifications inside of our source. And let's go ahead and create both a notifications controller, notifications service, and finally the notifications .module, which will wire this all together.

19:55

So, I'll start off with the controller. So, this will be a NestJS HTTP controller that we'll create with @Controller decorator. So, let's go ahead and export class NotificationsController. This will have a single post route here called notify. We can also add the notifications prefix to the controller decorator so that it's accessible at

20:18

this path. And so, notify will have the method then called whenever this post route is triggered. So, that will be the notify method here. We can also extract the HTTP request body with the @Body decorator, which I'll specify the shape of here. So, this will send in the title and the body as strings in the HTTP request

20:42

body. And now what we want to do is pass this through to the notification service. So, we'll add our constructor where we can inject it. Let's wire up the notification service next. So, I'll use the injectable decorator and export class NotificationsService. This will then inject in itself a constructor where we can then directly

21:05

inject the web socket gateway. So, that's the thing with the NestJS web socket gateway. They're like any other provider in our application. We can directly inject it into any other dependency, which makes it extremely useful because we have access to the web socket server and all the client connections here that we can use at

21:25

runtime whenever needed. And that's exactly what we're going to use. We're going to add a new send method where we pass in this input that has the title and body. And now we're going to create the notification object. We can also export this from the notification gateway so that we can reference it inside here and

21:48

have that type safety. So, we're going to build the notification object now that we're going to pass in and add the import directly. Make sure we've also fixed this typo. We have notification. This is missing an I. So, fix that as well in the broadcast method. And now in notification service, you can

22:09

see it's expecting the properties that we need to send here. So, for ID, I'll just use random UUID from crypto library. The title we'll pass in from the input directly. So, input.title. Same thing for the body. This will come directly from the HTTP request. And the timestamp I'll set to date.now. And now all we have to do to send this

22:36

to all of our clients is use the broadcast method and pass in the notification, which will send it to all connected clients on our web socket server, which is excellent. Now, to wire this up inside of the NestJS application, I've gotten rid of the notifications module directly. We can just add this to the app module directly

22:55

here. So, we'll add to our controllers array the new notifications controller and the providers array the notifications service, so that it's available in the application. And now our new HTTP route at notification/notify is available, and we can actually use it on our client side to start emitting events on the web socket server, which

23:18

is excellent. Finally, don't forget to go back to the notifications controller, and now we can go ahead and inject the notifications service of type notification service. And let's go ahead and make the call now to this.notificationservice.send and pass through the HTTP request body, which will include the title or body from the request itself.

23:46

And you can see here this expects to have some defaults, so we can pass in our own custom object where we pass the body title if it exists, or we just pass through a default text. In this case, I'll just say notification. And same thing for the body, we'll pass through the body text

24:06

or hello just as a sample. And make sure we fix this small typo here, so notification is corrected. So now at this point we're ready to actually set up a small client side web application that will establish web socket connection to the server and actually make requests to send new notifications and display any

24:28

notifications received on that same web socket connection. Let's go ahead and set this up next. All right, so to implement our client side implementation, I'm going to create inside of our project a new directory called public. And in here, I'm going to create an index.html file, which is going to be a simple HTML file that'll

24:50

include the ability to connect to our server through the socket.io client library, as well as the ability to send and receive messages from that socket and display it to the user on our HTML page. Now, I've also added a style.css file with some basic styles here just to make the application look a little bit

25:14

better. So, feel free to copy these styles over or change them however you'd like. I'm going to include those styles here. I've now gone ahead and filled in our index.html here and so let's walk through this in its entirety so we can understand what's happening. First part here is we're just setting up

25:32

basic HTML syntax. So, we have our HTML head with some meta, a title, and importantly, this is the link that I've included to our style.css file so that our styles will be loaded in. We also have a H1 here where we're displaying our system notifications and whether or not we're offline or online and we're

25:52

going to toggle this using JavaScript depending on our socket.io connection status. Next up here, we have a simple div with an input and a few buttons to allow us to actually implement some functionality. So, we have a button to send a new message back to our server. We also have a button to disconnect and

26:14

drop our connection on the socket. We have a button to reconnect. And next up, we have an unordered list which is going to be the feed and this is going to be where we're actually going to add on messages that we receive from the socket connection. So, to start, it is an empty unordered

26:31

list. And then finally, we also have a script tag here where we're loading in the socket.io library from a CDN. So, this is important. It's going to allow us to actually use the socket.io client constructor to connect to our web socket server. And that's exactly what we do in the next script tags. We open up our own

26:49

custom script tags so that we can write our own custom JavaScript. And the first thing that we're doing here is actually opening up our WebSocket connection to the server. To do this, we use the io function constructor, which takes in the address of the WebSocket server that we want to connect to. Now, in our case, this is going to be

27:11

the address of our Nest JS server. We provide at the root here, and that's going to be the same address that this client app is actually going to be served from, and we'll see how this works later on. But, essentially, we're pointing this at the root of our Nest JS server, where we know we have our

27:28

WebSocket server listening. And for this, we're just using location.origin, which is going to be the current URL that the web app is served from, which again is going to be the same one that the WebSocket server listens on. And remember, that's because we can have the WebSocket server listening on the same exact port as the web server. It just

27:47

listens for requests at a different path. And so, that's the magic of the WebSocket connection here, sharing the same path as the web server itself. We also override a few properties here like disabling reconnection, so that we can reconnect on our own. By default, Socket.IO will automatically try to reconnect whenever a connection is

28:09

closed. Next up here, we're just retrieving our feed and status elements. And then we have a function which creates a new list item and appends it to our feed unordered list. So, we're going to call this whenever we receive a new notification, so that we can add it to our list. And that's what we do here in this

28:31

system function, which simply just calls this row function, but also passes in the class of system, which applies a different CSS class because the notification in this case will be from the system and not from ourselves. So, that's all we're differentiating from here by calling row with the custom class. We will be appending it to the

28:53

unordered list with this class and make it stand out. We also have the set status function, which will check to see if we're online. It will attach the text and class name that will a- that that will show it as online or offline depending on the status. Next up, we actually apply some event

29:15

listeners to our open socket. So, we take the socket that we established up above and we have event listeners on it. So, we can listen for connection and disconnect events. And when this happens, we're simply calling the set status function with true or false, which will update our text and class accordingly depending on the connection

29:34

status of this socket. So, that's great. Finally, we have the ability to listen for notifications. And to do this, we use the socket.on function, which allows you to pass in the notification name or event name that you're listening for. Remember, this is the same exact name that we're emitting on the server side.

29:55

So, we're calling this notification, and that's what we need to listen for here as well. So, notification is the event we listen for, and whenever we receive a new one, we'll get the payload here in the callback, and we can do whatever we want with this. In our case, we are simply appending to

30:12

the text here the title of the new notification, and we're also looking at this property on the socket at this point in time we receive the notification called recovered. And this will automatically be set by Socket.IO if the socket has recently just reconnected from a disconnected state. And what this means is that if we are at

30:33

this point, it means that the socket just recovered, and any messages we're receiving at this point are being replayed from the socket. Now, this should not work and won't work when we run our application in memory, and for example, we try to reconnect a client, a different server, and we're going to see this shortly, as well as

30:56

how Redis Streams helps us solve this problem. Finally, we're setting up some click listeners. So, we have a click listener on the send button. Whenever we click on send, we're going to call at {slash} notify, and remember because this web server is going to be listening at the same address as our NestJS backend, we can simply call

31:17

{slash}. In this case, we want notifications, which, if you remember, is going to correspond to the notifications path prefix we set up. And then at {slash} notify is the endpoint where it's listening. So, we can send a fetch request to post a JSON body with the title of the notification here, which we extract from our input.

31:44

Last but not least, we have the ability to disconnect by adding an onclick listener here to drop. Whenever that is clicked, we are going to close the socket by calling socket IO engine close method. That will drop the connection. And then finally, the reconnect button is simply going to call connect on the socket.

32:05

So, now we have all of this functionality on the client side to test out our websocket implementation end to end. Let's go ahead and test this out. All right. So, to test this out, we of course need a way to actually serve this web application. And to do this, we'll use NestJS directly, which allows us to

32:23

do this very easily. And the first thing we'll do is provide a type here as we call NestFactory.create. Make sure it knows that it's using Nest Express application, which makes it an Express app. And what this allows us to do is to use the Express functionality called use static assets. Now, this is an Express feature that

32:46

allows us to serve static assets directly from our application here through the web server. And so, what we can do now is call join from path and point at process. dot current working directory to point to our current working directory in the application. And now we're just going to serve the public directory that we created. And

33:11

that will allow us to access the index.html through the >> [snorts] >> web server. So, let's go ahead and now test it out. If we head back into the browser, finally make sure we rename style here. This should be styles plural dot CSS. And now at this point, we can head back to the browser and try going to

33:33

localhost:3000 where the Nest server is running. You will see that we now have the index.html and the CSS applied here being served through the Nest server thanks to the use static assets, which is awesome. I'm also going to go ahead and open up another tab so we can have both of these running side by side

33:57

and actually see the web socket server in action. So, we'll have two instances of the client here. Let's go ahead and align these up just so we can actually see them. And now we can type whatever we want into this input here that gets sent to the web socket server. So, we can see both of

34:15

these are marked as online, meaning we've established the web socket connection. And we can even see this if we open up the network tab and refresh, for example, you can see we're making this request to our NestJS server, which is window.origin. localhost:3000. That's exactly where we supplied and then it's looking for /socket.io. That's the subpath that

34:40

socket.io NestJS gateway server is listening for for websocket connections. We're using the websocket protocol here and it automatically upgraded the connection to websocket and we can even see messages being sent over websocket here, which is awesome. Now, I'm going to go ahead and click send on this client here to send the notify request to our server and you can

35:04

see it's been populated here in the second client, the second tab, which is completely separated. So, this means that we did send the request to our server to actually notify and we can see that here in the request. We sent that post request with our payload and from there we are then broadcasting that

35:22

message to all connected clients, which includes this first connected client. We can even see that now on the websocket connection that it has. When we're pushing these messages, they show up here on the websocket connection that are being pushed data on this persistent connection. So, you can see as we push more data

35:42

onto the connection, we receive these new messages and they're displayed in real time. So, this is excellent. It means we now have this working end to end. And we can see the websocket connection working properly. However, let's look at a couple of problems with this architecture and what we're trying to solve with Redis streams. So, the first

36:03

of which is the following. So, let's go ahead and reset the state on both of them. Now, what happens if I send a notification? Fine, it pops up. But, what happens now, let's say we hit drop connection in the first tab, which is going to disconnect us from the websocket server and now let's say I'm

36:20

going to send three more notifications in this tab and now I'm going to click the reconnect button. Well, look at this. Now we've completely missed these messages. There's no way to get these messages back because this is an in-memory implementation that we have right now with our web socket server. It is a true pub/sub model where we publish

36:41

the web socket messages to all connected clients at that time, and then that's it. We don't replay or save any of these messages for later consumption, and that's the problem that we're trying to solve for as well as the multi-instance issue, and that's what I'm going to show you next. So, we have this fault tolerance that we

37:02

want to build in so that even if our clients go down, when they reconnect, they have the opportunity to consume any messages that they missed while they were offline. All right. So, now in order to demonstrate you the multi-instance horizontal scalability problem we have with our in-memory web socket server, I've gone ahead and added a new

37:23

directory called scripts with this dev cluster script, and I'm not going to go over this in too much detail, but essentially all it's doing is using ts-node, which is a tool to run TypeScript code in a local development server. All it's doing is simply executing our main TS server in NestJS using the node spawn script function.

37:47

And what this is going to do is allow us to spawn multiple instances of our NestJS server locally and demonstrate the multi-scalability issue with our web socket server. So, when we run this script, we're simply going to be running our dev server multiple times, and in this case we can also assign a port to

38:07

each one. So, for this example, we have port 3001 and port 3002. We then provide that port to the environment object through our port environment variable. And remember, our main TS reads this port environment variable in when it bootstraps the server. So, this is going to ensure we don't have a conflict for

38:27

our ports when we're starting these two development servers and allow us to do development servers running at once, exactly what we want. To run this, I've also added a new script to our package.json called dev, which is simply going to execute node and TS node against this new script. So, once you have this script copied over,

38:49

simply go ahead and now run PNPM dev command, which is going to go ahead and now start up both of our servers. So, you'll see below duplicate logs each time, and this is the two development servers both running in parallel. We can even see the addresses that they're both running at. And so, now what we can do, as it's

39:10

specified here, is open up two tabs to access both servers, one at localhost:3001, and the other at localhost:3002. These are two completely separate NestJS apps running that are serving our static application separately. Now, here is the underlying issue. If we click send in this tab, notice we no longer are seeing these notifications

39:35

pop up in the second instance. And why is this? Well, we know why this is, as we've explained, because, again, our web socket gateway that we've created is completely in memory. The first instance of our NestJS server here that's serving the app and the static web server, it has no knowledge of this second

39:56

application. So, when we click send here and broadcast the notification, all this is doing is sending the notification to all connected clients. Well, in this case, the first NestJS web socket server only has this one connected client, localhost:3001, so So only pushes the notification to this one. It has no knowledge of the

40:19

other NestJS app which contains a completely separate WebSocket server and connection to its own clients. And this is exactly the problem we're trying to solve for is this multi-instance scenario for our WebSocket server. And that's exactly what Redis Streams allows us to accomplish by establishing Redis as the persistent state for our WebSocket messages, we can

40:44

have multiple servers subscribe to that stream and whenever we push messages to that stream, then each instance and its respective clients will get all of them even if they're running under separate instances. And that's exactly what we're going to set up next as well as getting fault tolerance so that after we drop

41:02

connections and reconnect, we'll be able to read any messages from the stream that were published while we were offline. So let's go ahead in the next part of the lecture learn how we can set up Redis Streams to solve for both of these issues and have a truly resilient, production-ready WebSocket architecture. So setting up

41:23

NestJS Redis Streams is going to be so straightforward thanks to the adapter that we already installed earlier. To go ahead and get started inside of our WebSocket directory, go ahead and create a new Redis Streams adapter. And inside of here, we're going to export class Redis Streams adapter. This will actually extend IO adapter from

41:50

NestJS/platform-socket.io. And so this is an interface that will allow us to implement a couple of methods. In this case, we want the create IO server. Create IO server is going to get called when the notifications gateway is actually created. So instead of using the standard socket IO in-memory implementation, we can now use

42:17

this adapter, and this Redis streams adapter will be used instead. Create IO server will be called, and now we can create the actual Redis streams implementation, which uses Redis as the append-only log for any messages sent on the web socket server, so that we can make these available between all instances connected, and have fault

42:40

tolerance. So, the first thing we're going to do is create the underlying IO server of type server from socket IO. So, again, I'll import this type directly from socket IO, and we can also reference the server options type, which is going to be the type of the options that's supplied in here, so you can get

43:03

access to options that the, for example, gateway itself is supplying if you need these. Otherwise, to create the socket IO server, we can call remember, we're just extending the existing IO adapter. So, right now, this call here is just going to create the same IO socket server that'd be created automatically for us.

43:28

Now, here we then supply the port that the socket server is opened up under. So, that'll be by default the NestJS server port, unless the gateway specifies a different one. And then from here, we can also supply different options. So, we can spread all of the options that are being passed in from the gateway,

43:47

and then we can also specify any overrides that we want for the socket server itself. In our case, we can just keep the defaults. And so now we have just a regular old IO in-memory server. What we're going to do now is actually call server.adapter, which allows us to supply an adapter on

44:08

the socket server. And here we're now going to use the create adapter function. And this is the key function that actually ties in Redis streams. So, go ahead and import create adapter from @socketio/redis-streams-adapter. And so, now this function will return us back the Redis streams implementation that Socket.IO needs. This requires now a Redis client

44:38

connection object to actually connect to Redis. And that's what we need to create next is the actual client object to connect to our Redis server so that we can attach our socket gateway to it. So, let's go ahead and do this next by using ioredis to establish a simple connection to our Redis server that we have running in

45:00

Docker Compose. So, this is pretty straightforward. Inside of source, I'll just create a new redis.ts file. And now we can, for example, export const redis new redis from ioredis. This just takes in the URL that the Redis server is listening on. So, let's say, for example, we'll create the URL and set it equal to, in our case, we

45:26

know that's Redis is the connection protocol. And then in our case, we're going to connect to localhost:6380. Now, localhost:6380, if you remember from our Docker Compose, we're forwarding the local port 6380 to our Redis port on the container at 6379. So, this is going to forward requests to the correct Redis port inside of our

45:50

Redis container thanks to this Docker Compose port mapping. And so, now we have the URL, pass that into the ioredis connection object. And also pass an options object where I'll set lazy connect to false so that we connect immediately and determine any failures. So now we have the Redis connection object to connect to the

46:11

server. All we have to do is supply this now into create adapter so that it can actually connect to the Redis cluster and start producing and consuming data from that read-only ledger that it stores. Now, this is pretty much all we have to do. We can also specify options for the adapter, and this takes in some

46:32

interesting options. For example, you can actually give the stream a custom name here, which can be useful for debugging later on. We can just use the default of Socket.IO, and we also have this interesting property of max length. Now, max length here is the maximum size of the stream, and this is essentially

46:53

how many entries inside of this read-only ledger or stream we're going to allow. And this is an important attribute because if your stream, or in this case log, grows so large and you have so many entries, this is going to of course take up more disk space inside of Redis, and so you might want to control this and

47:13

control how large it can grow. Of course, if it's too small, then you're not going to have much of the benefit of being able to save these messages if you, for example, lose the connection to the server. So we'll just use the default of 10,000 here and leave the configuration object empty with just the

47:32

Redis connection. Just like that, we've connected Redis to our web socket server so that it can read and write messages from this append-only log. The last thing we need to do is wire it into the NestJS application. To do this, all we have to do is go back to the main.ts file and on the NestJS app, call use web

47:54

socket adapter. This allows you to pass in a adapter. So, we'll pass in new Redis streams adapter, the one we just created, actually instantiate it, and pass through the application here, which is what it expects. And now, as we can see from the description, this is going to be used inside gateways when we want

48:15

to override the default socket IO library. And so, now whenever we have this WebSocket gateway, it's going to be using this adapter under the hood to create the IO server, and in this case connected together to Redis. Now, finally, make sure we actually return the server back from this create IO server function, so that NestJS can

48:36

actually get access to it and associate it with our gateway. Additionally, we want to supply some options to our create IO server here. We want to associate our fault tolerance and turn this on. To do this, we need to supply the connection state recovery object. We're here we have this special property called max disconnect duration.

49:01

And I'm going to set this to, in this case, 2 minutes. And what this does is it tells the IO server how long it should allow disconnected clients that then reconnect to actually be able to recover and replay any messages that have been persisted to the Redis stream since it disconnected. So, by setting this to 2 minutes, we say

49:22

if the client reconnects before 2 minutes, then reread any messages that were sent in that time window, and then essentially republish them back to the client so that they appear to have not lost any messages. Now, of course, you can increase this more, and the user experience might be better because if they're

49:44

disconnected for more than 2 minutes, they'll still get any messages they missed, but of course, it's a balance because the longer we set this to, the more messages and data we have to save and persist to the Redis stream. So, let's go ahead and now test this out. So, now just like that, make sure

50:01

of course you have our Redis container running in your Docker Compose and I'll restart PNPM dev. This is going to launch both NestJS development servers, but now we've actually have our Redis adapter running inside of our WebSocket Gateway, which is awesome. And we can see this in action now automatically. I'm going to go ahead and open up our

50:23

application example again on localhost:3000 and localhost:3002. And now let's try the same example from before. If I click on send notification in this first client, which is a completely different server, we are now seeing the notification in the second tab, even though this is running on a completely different NestJS server with a totally different client

50:48

connection object to the WebSocket server. And so, this application doesn't even know about this client at all. But since now we are appending these notification messages to the Redis stream, and then this instance will then be consuming from that and emitting to its local clients, we now have a fully integrated WebSocket server for multiple

51:11

instances. Now, let's go ahead and also test out the reconnection and fault tolerance. If I go ahead and send a message that shows up, but if I drop the connection now and then send a few more messages and then reconnect, notice that these messages then get replayed. The client object is marked as recovered, and we

51:32

actually get the data republished back here to this client thanks to the Redis stream. When we reconnect, we're within side of that reconnection window, so we can actually reread data that was persisted to the Redis stream since this went offline, and now we have no data loss even when our clients go offline.

51:52

So, we have amazing fault-tolerant WebSocket connections that are fully scalable. We can have as many instances as we want, all connecting to the Redis stream, receiving and sending messages together with this fault tolerance. So, this is really great to see. We now have all of this working exactly the way we want. I hope you

52:13

learned so much in this lecture about production-grade WebSocket architectures, especially when we need to scale upon more than one instance. So, thank you so much for watching, and I'll see you in the next one.

Transcribe another video

Paste any YouTube, Instagram or TikTok link to get a free transcript.

Free · No sign-up · Unlimited