Scaling GraphQL Subscription with Apollo Server and ElastiCache for Redis
Tags: graphql, redis, pubsub
Published: 2021-04-28 07:51:23

As the GraphQL application grows and backend architecture requires to scale into high availability structure such as Dockers, you will need to make sure your Apollo Server support scaling horizontally. Otherwise, your GraphQL Subscription will break due to WebSocket Connection failure.

GraphQL subscription on a single instance architecture is pretty straight forward as you only have single WebSocket server so subscription events are centralized by nature, client connection always will be listening to the same PubSub source. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7n72evrpmdk28kzbqbgf.png)

But, when you have multi instances spun up for GraphQL Server (ex. Apollo), you will turn out having multiple WebSocket connection with its own PubSub. It is not ideal and problem arises when u have multiple PubSub server.

For example, Client A and Client B in the same chat room subscribing to the same Channel might be connecting to different WebSocket instance. Thus, you won't be receiving event in the other instance when either client publishes event to the WebSocket that he/she is on.

Hence, you will need to centralize the publication of the subscription events in a PubSub server so that all instances can subscribe to and receiving all them across instances.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xrwjbnl1o9jgj1mujytn.png)

A multi instance architecture helps to increase reliability and create elasticity to horizontally scale your application as the number of clients grow. 

To test locally, set up `redis-server` and `redis-cli` locally so you can connect your local server to local Redis PubSub instance. See https://gist.github.com/tomysmile/1b8a321e7c58499ef9f9441b2faa0aa8 for local Redis Setup

Then, in your GraphQL Server application add a PubSub file.
```typescript
// PubSub Server (server/pubsub.ts)

import Redis from "ioredis";
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { PubSub } from "apollo-server-express";

const redisOptions = {
  host: REDIS_HOST_URL, // set to localhost for local redis
  port: 6379
};

const dateReviver = (_key, value: string) => {
// By default, Javascript objects are serialized using the JSON.stringify and JSON.parse methods. 
// This means that not all objects - such as Date or Regexp objects - will deserialize correctly without a custom reviver, that work out of the box with the default in-memory implementation.
// For handling such objects, you may pass your own reviver function to JSON.parse.
};

export const pubsub = process.env.NODE_ENV === 'development' ? new PubSub() : new RedisPubSub({
  publisher: new Redis(redisOptions),
  subscriber: new Redis(redisOptions),
  reviver: dateReviver
});
```

Add PubSub to your Apollo Context, so that subscription resolvers can use use it conveniently, to perform publication.

```typescript
// Apollo Server (root/index.ts)

import { pubsub } from './server/pubsub';
import { ApolloServer } from "apollo-server-express";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req, connection }) => {
    if (connection) {
      return {
        ...connection.context,
        pubsub,
      };
    } else {
      return { pubsub };
    }
  },
  subscriptions: {
    path: '/subscriptions',
    onConnect: () => console.log('Connected'),
    onDisconnect: () => console.log('Disconnected'),
  },
)};
```

Subscription Resolver:

```typescript
// chatSubscription resolver/subscription/chat.ts

const resolvers = {
  Subscription: {
    chatCreated: {
      subscribe: (_parent, _args, { pubsub }) => pubsub.asyncIterator(['CHAT_CREATED']),
    },
  },
  // ...other resolvers...
};
```
For production you may use Amazon ElastiCache for Redis as managed service. Here's how to set it up: https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/GettingStarted.html. Once created, you will refer to the generated URL and use it for `REDIS_HOST_URL` env.

Before deploying the changes, make sure you have update ElastiCache's security group to accept inbound traffic from your Dockers/EC2 instances. Otherwise, you might get TIMEOUT error while `pubsub.ts` trying to connect. Simply do this by white-listing `Dockers/EC2` security group in ElastiCache's inbound rules.

> Note that you cannot connect ElastiCache to local server for testing due to default security configuration. Unless you set up VPN for secured connection to the ElastiCache: https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/accessing-elasticache.html#access-from-outside-aws

###Reference:
- https://aws.amazon.com/blogs/database/how-to-build-a-chat-application-with-amazon-elasticache-for-redis/
- https://github.com/davidyaha/graphql-redis-subscriptions