Modernizing Our Video Analytics Stack: From Rails to Node.js

Written by AVEQ Team on January 13, 2025

tech

We recently completed a significant migration of our Surfmeter Client Analytics backend from Ruby on Rails and Postgres to a modern Node.js-based architecture based on Express and BullMQ. Here’s how we approached this migration and what we learned along the way. We hope this blog post will be useful to others who are considering a similar migration!

Our Existing Stack

Surfmeter is based on a client-server architecture, where our clients are Surfmeter-equipped devices communicating with a central server. Our original stack has served us well for years, handling video quality, web performance, and network-based measurements for major ISPs — primarily based on an active probing approach. Here, the use case consists of dozens or maybe hundreds of probes submitting their measurement data on a scheduled basis to our central server. We use the following stack:

Our stack before the migration.

Data persistence is handled both by Postgres and Elasticsearch, but we occasionally clean up the database after everything’s been indexed into Elasticsearch. Our database holds more granular data, but we do not need to keep it for a long time.

You might ask: why Rails?  It’s just what we are used to. When you build software in a startup, it is vital to not waste time on adopting the latest and greatest technology just for the sake of it — instead, you have to build on what you know best and what gets the job done. Rails still is, in our opinion, a great framework for building featureful web applications, where you can iterate quickly. And we’re still proud of our app, which is covered extensively by unit tests, and has never failed us so far.

Why We Migrated

Last year we introduced a new set of SDKs for client analytics — allowing content providers to directly integrate Surfmeter-based quality monitoring into their own Android, iOS, and web-native implementations. This is a different use case compared to the active probing scenario: now, several thousands of clients are equipped with our SDK to continuously send their measurement data to our server, and this type of data is largely unpredictable. It is sent more frequently, and more clients are active during peak times.

As our measurement volume grew and real-time processing requirements became more demanding, we needed an architecture that would:

As we deployed our existing Rails-based stack for a first customer, we noticed issues with high CPU load and latency for measurement processing. We found that there were two main bottlenecks in the current setup for this type of workload:

As data volumes grew more and more — and with the help of profiling tools like Elastic APM —  we found the main culprit to be the SQL database: during storing of the objects, we SELECT and INSERT a lot of data from various tables. These queries became slow over time, due to a high number of JOIN statements needed. Essentially, we found that the database was not able to handle the load, even when giving the server more CPU resources.

To provide short-term fixes, we carefully examined our options and tried to start with the most effective solutions first. It’s the Pareto principle at work, again! We did the following:

But overall, the problems were not solved, and we reached a point of diminishing returns. We certainly wouldn’t be able to handle twice as many clients, let alone 10x more. So we went for something completely different!

Our New Stack

We thought about how to best handle the high-throughput measurement ingestion and the complex data processing requirements. Based on research on what others in the field typically do, and talks with experts, we decided to go for a Node.js-based server with a Redis-first architecture. NoSQL, essentially. At the core, it was important to eliminate the SQL database. In fact, we were sure we wouldn’t even need it in the long run, because for client analytics-type data, persistence in the ELK cluster was sufficient. The raw data had to be persisted only temporarily while statistics were being calculated, and could then be evicted.

Our stack after the migration.

But first, the server: the reason for migrating to Node.js was that, primarily, our client libraries are written in TypeScript, and so we would benefit from a more consistent developer experience, where we could share types between the clients and server.

In general, in the last few years, we’ve found TypeScript to boost our productivity and code quality, compared to Ruby, which has very little type safety and lets you make silly mistakes all the time. This has become better with the Ruby LSP and tools like Sorbet, but it’s still not as good as TypeScript.

In general, Node.js is a faster engine for handling requests compared to a plain Ruby server. We settled for good old Express.js, which is a mature and battle-tested framework for building web servers. There might be faster servers — but we have to test them first. Libraries like Zod help with request validation, and we use a custom logger (built on top of Pino) to handle request and response logging in a consistent way.

Focusing on the client analytics use case only, we could also simplify the data model, because we could now store all data with a low footprint in Redis. Every measurement just became one hash entry in Redis; we would only need to persist the data in Elasticsearch for longer-term storage, and could then delete the data from Redis automatically via its expiration mechanism. Being able to reuse Redis would also mean very little maintenance overhead for us, as we already have a Redis cluster in place and configured. We would only have to swap out the web server component, and keep the remaining services running as-is (perhaps with some minor tuning in terms of resource consumption).

The SQL database could still be used for storing infrequently changing data like API keys. Since these could be cached in-memory, we wouldn’t need to query the database often.

Since Sidekiq does not work with Node.js, we had to find a replacement for the job processing. We found that BullMQ was a good fit for our needs. It is a TypeScript-based library that works with Redis as a backend. Porting our worker jobs over from Rails was quite straightforward, because we had 100% test coverage for the existing stack, and we knew what output we expected from the new workers. Furthermore, our video-quality related statistics code was available already in the form of native JavaScript code, so we could skip calling a microservice or command-line program, and instead execute each calculation directly, within less than 10 milliseconds, compared to the 200ms+ it took with Rails/Sidekiq (with all the network and database overhead).

LLMs and AI-assisted coding tools like Anthropic’s Claude and Cursor have helped us with migrating the core functionality, by referencing the existing code and using the type structure to guide the LLM in creating the right outputs. Right at the start of the migration, we set up a test framework to ensure we produced the same data as with our old stack — testing with vitest has been a breeze. In general, the whole developer experience was positive for us, with bugs found early due to the strong type system offered by TypeScript and BullMQ.

Results, and Where To Go From Here?

We wrote the whole new pipeline in less than 10 days! This included the server, the job processing, and the data model. We were able to migrate all our existing clients to the new system, and we’re now happily ingesting data and watching as the new workers process the measurements.

The CPU load has dropped significantly, from an almost unbearable 100% to a more manageable 5-10% on average. Memory consumption is also much lower, since most of it was held by Postgres. Our memory footprint was reduced by 75% or more. This means we can now easily scale to 10x the clients on the same hardware — and that is without any performance tuning.

This is more like what we want to see on a server.

Of course, our journey is not over yet. Here are some important paths forward:

But these are topics for another blog post!

So, to summarize, this migration set us up for future growth and enables us to:

The new Node.js-based architecture provides a solid foundation for our video quality measurement platform, allowing us to better serve our ISP and telco customers with more reliable and scalable analytics.