
Server rendering benchmarks: Railway vs Cloudflare vs Vercel
A couple of weeks ago, independent developer Theo Browne published a set of benchmarks comparing server-side rendering (SSR) performance between Cloudflare Workers and Vercel Functions.
At first, the tests showed Cloudflare lagging behind — about 3–5× slower than Vercel Functions. But then Cloudflare rolled out a wave of performance improvements, flipping the script and, in some cases, pulling ahead.
We were intrigued by this whole situation and wanted to see how Railway stacks up when running the same benchmarks. These were the results we got (lower is better):

Benchmark | Cloudflare (ms) | Vercel (ms) | Railway (ms) | Result |
Next.js | 1453 | 1111 | 1809 | Vercel wins — 1.31× faster than Cloudflare, 1.63× faster than Railway |
React SSR | 155 | 183 | 191 | Cloudflare wins — 1.18× faster than Vercel, 1.23× faster than Railway |
Sveltekit | 296 | 368 | 114 | Railway wins — 2.60× faster than Cloudflare, 3.23× faster than Vercel |
Math | 574 | 701 | 1166 | Cloudflare wins — 1.22× faster than Vercel, 2.03× faster than Railway |
Vanilla JS | 1831 | 1878 | 425 | Railway wins — 4.31× faster than Cloudflare, 4.42× faster than Vercel |
Railway performed strongly in two of the five benchmarks — performing the best in the SvelteKit and Vanilla JS tests, where it ran roughly 3–4× faster than both Cloudflare and Vercel. The React SSR results were close, while the Next.js and Math benchmarks highlight areas for further optimization, which we’ll cover in this post.
We’ll start by looking at what the benchmarks actually test, how we added Railway into the mix, and how we ran our setup. After that, we’ll break down each platform’s deployment model and scaling approach before diving into the benchmark results.
The benchmarks test SSR performance by dynamically rendering a data-heavy page that performs thousands of mathematical calculations — primes, Fibonacci numbers, factorials, and nested data structures — creating a computation-intensive workload that pushes both CPU and rendering limits.
Each framework implements the same workload, but in its own way:
- Next.js: React component using JSX
- React SSR: Identical logic as the Next.js benchmark but implemented via
React.createElement()
without JSX - SvelteKit: Logic runs in
+page.server.ts
, rendered using Svelte templates - Vanilla JS: HTML built manually via string concatenation.
The Vanilla JS implementation also includes two heavier variants:
/slower-bench
— ~3× heavier workload (150 sections vs. 50, 60 items each vs. 20, and a prime limit of 500,000 instead of 100,000)/realistic-math-bench
— focuses on integer arithmetic, array sorting, string hashing, and prime counting
The benchmarking process is orchestrated by a script that executes 400 HTTP requests per endpoint with 20 concurrent connections, measuring full round-trip response times — from request start to fully rendered response.
For each test, it records:
- Average latency
- Fastest response
- Slowest response
- Variability (difference between fastest and slowest results)
- Success rate
Once all measurements are collected, the script ranks the platforms for each workload and generates comparison reports summarizing relative speed and consistency.
We forked Theo’s benchmark repository and created a dedicated railway-edition
version for each test. Code changes were minimal — just defining entry points and adding start commands. We also updated the benchmarking script to include Railway as a platform.
For the Next.js, React SSR, and SvelteKit benchmarks, we used Bun as the runtime. The vanilla benchmark, on the other hand, ran on Node.js (more details on that later).
Finally, each variant was deployed as its own service and scaled to 10 replicas (more on that later).

To keep results easy to reproduce, we ran the test client directly from an m7i-flex.large
EC2 instance in AWS’s us-west-1
region (California)

Running benchmarks from an EC2 instance from AWS’ CloudShell
Each benchmark on Vercel was deployed to the us-west-1
region as its own project using default compute settings — 1 vCPU and 2 GB memory — since all benchmarks were single-threaded.
Each benchmark on Vercel was deployed as an independent project in the us-west-1
region, using the default compute configuration of 1 vCPU and 2 GB of memory.

Region configuration for Vercel functions
For testing Cloudflare, we used Theo’s test account: theo-s-cool-new-test-account-10-3.workers.dev
. Cloudflare Workers automatically run code in the region closest to the request and are well-connected to all AWS regions, so no region configuration was required.
Finally, all Railway services were deployed in our US West region. Our infrastructure runs on globally distributed hardware that is fully owned and managed by Railway across data centers worldwide.

Region configuration for Railway services
This setup minimizes network latency as much as possible. That said, because Vercel’s infrastructure also runs on AWS — and our EC2 test client was in the same region as its functions — this gives Vercel a slight edge.
Before diving deeper into the benchmark results, it’s important to understand how each platform deployment and scaling models.
Cloudflare and Vercel both follow a serverless model — you write your code, deploy it, and the infrastructure is abstracted away from you. Under the hood, though, the two platforms work quite differently.
When you deploy on Cloudflare, your code runs on their global network, which spans thousands of machines across hundreds of locations. Each machine runs workerd
, a custom runtime built on the V8 engine. One of Cloudflare’s value propositions is you don’t need to choose a region — the platform routes each request to the nearest data center, running your code as close as possible to the user for optimal latency.
As your app scales and handles more requests, Cloudflare automatically distributes the workload across its network.

Cloudflare locations
Vercel, on the other hand, runs on AWS and takes a slightly different approach to deployment and scaling. Server-side logic is deployed as AWS Lambda functions under the hood. Vercel automatically creates new function instances to handle incoming requests, allowing multiple concurrent executions within each instance. As traffic increases, it scales by spinning up additional instances to meet demand. (see Fluid Compute for details).
Over time, idle functions automatically scale down to zero, reducing unnecessary compute usage.

Source: vercel.com/docs/fundamentals/what-is-compute
Both platforms charge only for active CPU time — you’re not billed for I/O waits, network latency, or external API calls.
While the serverless model makes deployment simple by abstracting away infrastructure, that simplicity comes with trade-offs. There are limits on memory, execution time, and function size. You can’t run long-running workloads or those requiring a persistent connection, and you give up control over the execution environment, relying entirely on the platform’s predefined runtimes.
Serverless emerged as a response to the friction of managing servers. You need to choose instance sizes, CPU, and memory before knowing what your app actually needs. This guesswork often leads to one of two outcomes:
- Under-provisioning: not enough compute, causing slow or failed requests.
- Over-provisioning: too many idle resources, which you pay for regardless of your usage
At Railway, we believe the answer isn’t to hide the server — it’s to make the server experience better.
Services you deploy on Railway run on long-running servers. You can import your code and we’ll build and deploy it for you, or you can deploy directly from an OCI-compliant image registry (e.g DockerHub, GitHub image registry, etc.). You have full control over the runtime and language your service uses.
Creating a new project on Railway
Deployed services automatically scale up or down based on workload — no need to pick instance sizes or tune scaling thresholds.
You can also scale horizontally by adding replicas. Railway automatically balances public traffic across replicas within each region.
Additionally, replicas can be deployed in multiple regions. Railway routes incoming traffic to the nearest region and evenly distributes requests among the replicas there — all without any manual scaling configuration.
Each replica runs with the full compute limits of your plan. For example, on the Pro plan, services can use up to 32 vCPUs and 32 GB RAM. Deploying three replicas gives your service a combined capacity of 96 vCPUs and 96 GB RAM.
Deploy replicas both within the same region and across different regions to scale
Pricing follows the same principle as Cloudflare and Vercel: you pay only for active CPU and memory usage, not idle time. When running multiple replicas, you’re billed only for the compute time actively consumed by each one.
This means you can get a similar experience to serverless without the constraints around memory, file sizes, or execution limits.
Now that you have a an understanding of the deployment and scaling models of each platform, let’s do a deeper dive into the benchmark results.
Previously, Railway was running on Google Cloud, and we recently migrated to our own baremetal servers earlier this year.
We’ve built our entire platform from the ground up, giving us full control over every layer of the stack — hardware, networking, software, runtime, and orchestration.
We’re now deploying the next generation of our hardware, unlocking even better performance across our infrastructure. You can learn more about our data center buildout in our blog posts:
Unsurprisingly, deploying more replicas on Railway led to better overall performance across all benchmarks. This makes sense since the workload is now split across more instances. Here are the results of the benchmark when running at 20 replicas instead of 10:

Benchmarks when Railway services run on 20 replicas instead of 10
You’ll find that Railway is pretty much faster across all benchmarks. Of course, there are diminishing returns. It’s better to understand the workload you’re running and to optimize something else instead of throwing more compute at the problem.
Unfortunately, Next.js doesn’t fully utilize the compute resources available when deployed on Railway. The framework is currently limited to using a single CPU core — a detail confirmed by Vercel’s CTO and verified in our own testing.

https://x.com/cramforce/status/1975656443954274780
As a result, the only effective way to scale a Next.js app is through horizontal scaling, by adding more replicas rather than relying on additional CPU cores.
Additionally, while Next.js is one of the most popular frameworks for building web applications, it’s been historically difficult to deploy it effectively outside of Vercel. This led to the community to create OpenNext, a shared effort to make the open-source framework work reliably everywhere. Both Cloudflare and Netlify now maintain their own adapters built on top of this work.
Fortunately, the Next.js team is now formalizing this with official Deployment Adapters — a standardized API that makes it easier for platforms to integrate Next.js without hacks or reverse engineering. Vercel will use the same adapter API as everyone else, ensuring true parity across environments. You can follow the proposal in this RFC.
With these updates, deploying Next.js on Railway should become much smoother and more performant.
Railway is language, framework, and runtime agnostic — giving us the flexibility to experiment with different configurations and identify the most optimal setup.
The Vanilla JS and Math benchmarks are served from the same service. Our initial tests used Bun as the runtime, which performed about 5% faster than Node for the Maths benchmark. However, for the Vanilla JS benchmark, Node actually delivered 8.5% better performance. We’ll continue investigating these results and collaborate with the Bun team to dig deeper.
The Cloudflare team also found something interesting: Node.js isn’t currently using the fastest available implementation for certain trigonometric functions. Since Node supports a wide range of systems and architectures, it’s compiled with more conservative defaults. V8, the JavaScript engine it runs on, includes a compile-time flag that enables a faster path for these math operations. In Cloudflare Workers, that flag happens to be on by default — in Node.js, it’s not. The Cloudflare team has opened a pull request to enable it, which should make math-heavy workloads faster for everyone once it lands. You can learn more in their blog post.
Finally, we found that container runtimes introduce a noticeable performance penalty — roughly 40% compared to running natively. This lines up with the weaker math benchmark results we observed earlier.
We’re already working on a new VM runtime designed to provide stronger isolation and significantly better performance. As it matures, we expect to close much of this gap — and we’ll share more about it in a future post.
We believe that having open benchmarks are valuable for everyone. They help platform builders like us identify where we can improve, and they give developers a clearer sense of what to expect when deploying their workload.
That said, benchmarks are tricky. Designing tests that truly reflect real-world performance is harder than it looks. The numbers you see often represent only a small slice of the full picture — in this case, CPU performance. In practice, application performance is influenced by a lot more: database speed, network latency, API calls, and even end-user connection quality.
We’re investing in creating more open benchmarks to better capture these real-world conditions. And if you’ve run your own tests where Railway performs differently, we’d love to see them — we’ll dig in, learn from them, and keep improving.
If you have any questions or feedback, you can tag us on X, reach out in Help Station or ping us on Discord.