---
title: "How We Improved Network Speed By 300% In Cypress 3.3.0"
url: "https://www.cypress.io/blog/how-we-improved-network-speed-by-300-in-cypress-3-3-0"
description: "When a user runs tests in Cypress, all of their application's HTTP requests go
through the Cypress HTTP proxy. Why? Well, when a website is loaded through the
proxy, Cypress injects code that helps with the testing process. Among other
things, Cypress's proxy:

 * injects a global window.Cypress object that can be used by the application
   under test to determine if it is running inside of Cypress.
 * removes framebusting [https://en.wikipedia.org/wiki/Framekiller] JavaScript
   so that Cypress"
---

[Blog](/blog/)

# How We Improved Network Speed By 300% In Cypress 3.3.0

May 22, 2019

•

By The Cypress Team

When a user runs tests in Cypress, all of their application's HTTP requests go through the Cypress HTTP proxy. Why? Well, when a website is loaded through the proxy, Cypress injects code that helps with the testing process. Among other things, Cypress's proxy:

-   injects a global `window.Cypress` object that can be used by the application under test to determine if it is running inside of Cypress.
-   removes [framebusting](https://en.wikipedia.org/wiki/Framekiller?ref=cypress-io.ghost.io) JavaScript so that Cypress can properly run the applicatin under test within an iframe.
-   replaces absolute references to `window.top` and `window.parent` to the correct frame reference (since `window.top` points to Cypress's `window` instead of the application's `window`).

Also, in the near future, the proxy will allow Cypress to support [full network stubbing](https://github.com/cypress-io/cypress/issues/687?ref=cypress-io.ghost.io). This will allow users to mock and assert on _any_ HTTP request from _any_ origin; from the `fetch` API to a page load. Heck, it could even be used to test that an Adobe Flash applet is making the right requests under the hood.

It's easy to see that Cypress's proxy is a necessary and valuable part of Cypress as a whole - without it, Cypress simply couldn't offer the testing experience that it does now.

## The problem: it was slow

In Cypress 3.1.4, [@danielschwartz85 reported that they were seeing network slowness while making normal `fetch` requests](https://github.com/cypress-io/cypress/issues/3192?ref=cypress-io.ghost.io). It was so bad that they reported load times of _5.5 seconds_ in Cypress, compared to _300ms_ in a regular browser - HTTP requests in Cypress were running up to **18 times slower** than in Chrome for this test case.

A large part of Cypress is that it should behave just like a normal web browser, so this was a major issue. Users expect Cypress to "behave like Chrome", not "behave like Chrome but 18x slower". So, we set out to investigate the source of this slowness.

## Investigating the problem

The source of the slowness was not obvious, so we needed to gather some more information. First, we wanted to understand what speed we should aim for to match Chrome's normal behavior.

We extended the bug report's original test case to create [a web page](https://test-page-speed.cypress.io/index1000.html?ref=cypress-io.ghost.io) that loads 1,000 copies of the movie poster for [Smurfs: The Lost Village](https://www.imdb.com/title/tt2398241/?ref=cypress-io.ghost.io). The time taken to load this page and all 1,000 images would become our benchmark for all future network performance tests.

## Instrumenting network performance

Our next step was to automate testing Google Chrome's performance.

We decided that we wanted to test all the possible combinations of the following network conditions that may influence page load speed:

-   Running in regular Chrome vs. Chrome behind the Cypress proxy.
-   Running behind a secondary HTTP proxy vs. running without an HTTP proxy.
-   Running with HTTP/2 support enabled vs. running with HTTP/2 disabled.
-   Forcing Cypress to intercept and rewrite all requests vs. just passing them through Cypress.

We automated these tests by creating a script that does the following:

1.  Launches a headless Chrome instance using Node's [`child_process.spawn()`](https://nodejs.org/api/child_process.html?ref=cypress-io.ghost.io#child_process_child_process_spawn_command_args_options).
2.  Attaches [`chrome-har-capturer`](https://github.com/cyrus-and/chrome-har-capturer?ref=cypress-io.ghost.io) to that Chrome instance. `chrome-har-capturer` allows us to capture an [HTTP Archive (HAR)](http://www.softwareishard.com/blog/har-12-spec/?ref=cypress-io.ghost.io) that contains all the details of HTTP requests made in Chrome including headers, timings, and full request and response bodies for later analysis. This was extremely valuable - a HAR contains data that is normally only viewable manually in [Chrome's Dev Tools Network interface](https://developers.google.com/web/tools/chrome-devtools/network/reference?ref=cypress-io.ghost.io).
3.  Loads the thousand-Smurfs test case in a new tab.
4.  Once no network traffic has occurred for more than 1 second, aggregates the HAR timing data, creating statistics like "total page load time" and "average time-to-first-byte (TTFB)".
5.  Repeats steps 1-4 with all of the network conditions described above.

You can see the resulting test script here: [packages/server/lib/proxy\_performance.js](https://github.com/cypress-io/cypress/blob/6d5963b9ca6c09127bb364e4497a32bd7b47c4e6/packages/server/test/performance/proxy_performance.js?ref=cypress-io.ghost.io). We then ran these tests on every single run in CI, so we constantly got feedback on the performance impact of changes to the networking code.

## Interpreting the results

In our hubris, the first hypothesis as to why our proxy was so slow was simply:

> Oh, Chrome just runs slower behind a proxy `¯\_(ツ)_/¯`

This was quickly disproven, as testing showed that _our_ proxy was 3x-5x as slow as just a normal HTTP proxy:

| Test Case | 1,000-Smurfs Load Time (ms) |
| --- | --- |
| Chrome (HTTP/1.1 only) | 4620 |
| Chrome behind a proxy | 3729 |
| Chrome behind Cypress (v3.1.5) | 9630 |
| Chrome behind Cypress (v3.1.5) with interception | 16827 |

The next thing we noticed is that the average time between Chrome sending a request and receiving a byte back from the server (TTFB, [from `timings.wait` in the HAR](http://www.softwareishard.com/blog/har-12-spec/?ref=cypress-io.ghost.io#timings)) was _significantly_ higher when running in Cypress than it was for Chrome:

| Test Case | Average TTFB for 1,000-Smurfs (ms) |
| --- | --- |
| Chrome (HTTP/1.1 only) | 20 |
| Chrome behind a proxy | 20 |
| Chrome behind Cypress (v3.1.5) | 55 |
| Chrome behind Cypress (v3.1.5) with interception | 56 |

We couldn't come up with an acceptable explanation for this gap. In fact, we decided that this gap was the _most likely_ culprit for the network slowness, based on the following logic:

-   55ms is 35ms _slower_ than 20ms.
-   35ms slowdown × 1,000 images = 35,000ms of slowdown.
-   [Chrome will make up to 6 concurrent connections to a single HTTP origin](https://superuser.com/questions/499114/how-to-increase-chromes-connections-per-server?ref=cypress-io.ghost.io). Thus, this TTFB lag causes 35,000ms ÷ 6 ≈ 5,833ms of total slowdown in a single 1,000-Smurfs test run.

> If you are curious about the results of the performance testing, [check out this GitHub comment for a full run-down](https://github.com/cypress-io/cypress/pull/3531?ref=cypress-io.ghost.io#issuecomment-471617949).

## Fixing slow time-to-first-byte in node

At this point, we had the following pieces of information:

-   TTFB was significantly slower when using our proxy
-   It was only happening on HTTPS requests

To understand the solution to the problem, you'll first need to understand how Cypress's proxy intercepts HTTPS traffic. HTTPS traffic is normally totally encrypted between the client and server. Once the initial handshake completes, no man-in-the-middle can modify or read the data being transmitted without breaking this encryption and corrupting the stream.

The traditional implementation of HTTPS over an HTTP proxy preserves this encryption like so:

1.  User visits `https://example.com` in Chrome behind the HTTP proxy `http://127.0.0.1`.
2.  Chrome will send a `CONNECT example.com:443` request to `http://127.0.0.1`.
3.  The HTTP proxy will create a TCP/IP socket to `example.com` on port `443` and return `200 Connection OK` back to Chrome.
4.  From this point on, the proxy at `http://127.0.0.1` transparently shuttles bytes between Chrome and `https://example.com`, enabling Chrome and `https://example.com` to perform the HTTPS handshake and continue their encrypted conversation.

**Here's a flowchart of this conversation to help visualize:**

[![Flowchart of a normal HTTP proxy proxying HTTPS traffic](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2019/05/normal-proxy-flow.png)](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2019/05/normal-proxy-flow.png?ref=cypress-io.ghost.io)

But, as discussed earlier, Cypress is not a normal HTTP proxy. Cypress needs to unpack and intercept HTTPS requests so that it can do the necessary injections. Cypress has no idea what the destination server's private keys are, so how can it do this without corrupting the encrypted data?

Simple: Cypress stands up an internal HTTPS web server using Node's [`https.createServer`](https://nodejs.org/api/https.html?ref=cypress-io.ghost.io#https_https_createserver_options_requestlistener) and _self-signs SSL certificates_ for _every HTTPS destination_ a user wishes to reach. Then, instead of proxying directly to the destination HTTPS host as described in step (3) above, Cypress proxies the user to the _internal_ HTTPS web server, and that HTTPS server receives the user's request as a normal Node.js `ClientRequest` object. From there, Cypress can make an outgoing request to the user's actual destination using the [`request` HTTP library](https://github.com/request/request?ref=cypress-io.ghost.io) and intercept the request or response as necessary.

> Want to see how Cypress self-signs SSL certificates for on-demand HTTPS interception? [Check out Cypress's `https-proxy` package where all HTTPS traffic is handled.](https://github.com/cypress-io/cypress/tree/develop/packages/https-proxy?ref=cypress-io.ghost.io)

**Here's a flowchart to help visualize Cypress's HTTPS interception:**

[![Flowchart of Cypress's HTTPS interception](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2019/05/cypress-proxy-flow.png)](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2019/05/cypress-proxy-flow.png?ref=cypress-io.ghost.io)

So, somewhere in here, TTFB is getting inflated unnecessarily. We quickly eliminated the certificate signing as the source of the lag - Cypress caches the certificates in memory and on disk, so there should not be a repeated slowdown there.

That means that the only source of slowness (besides network round trip time) is the pipe that shuttles bytes between Chrome's socket to the Cypress HTTP proxy and Cypress's socket to the internal HTTPS server (highlighted in red in the flowchart above).

After hours of head-scratching at how a seemingly simple operation could be causing _so much lag_, [@brian-mann](https://github.com/brian-mann?ref=cypress-io.ghost.io) discovered [`socket.setNoDelay()`](https://nodejs.org/api/net.html?ref=cypress-io.ghost.io#net_socket_setnodelay_nodelay) while trawling the Node docs for answers. Here's the description of what this does:

> Disables the Nagle algorithm. By default TCP connections use the Nagle algorithm, they buffer data before sending it off. Setting `true` for `noDelay` will immediately fire off data each time `socket.write()` is called.

**[Nagle's algorithm](https://en.wikipedia.org/wiki/Nagle%27s_algorithm?ref=cypress-io.ghost.io)** is a TCP congestion control mechanism that works by decreasing the number of small packets sent over a network by buffering multiple sequential packets and sending them out only when the recipient has acknowledged the previous packet.

As soon as we added the `socket.setNoDelay(true)` call on the socket between Cypress and the browser, we immediately saw huge performance improvements:

| Test Case | Avg TTFB for 1,000-Smurfs (ms) | 1,000-Smurfs Load Time (ms) |
| --- | --- | --- |
| Chrome (HTTP/1.1 only) | 20 | 4620 |
| Chrome behind a proxy | 20 | 3632 |
| Chrome behind Cypress (after disabling Nagle) | 20 | 4169 |
| Chrome behind Cypress (after disabling Nagle) with interception | 24 | 4458 |

Nagle's algorithm was the major source of this TTFB lag. It was causing unnecessary buffering that meant that we had to wait for 2 extra round trip times before the first byte reached the client. By just disabling it, our performance became almost the same as a real browser. This is a massive win for users of Cypress, especially considering that a single spec file may reload the same page tens if not hundreds of times.

## Other network improvements in 3.3.0

In addition to the speed boost, [Cypress 3.3.0](https://on.cypress.io/changelog?ref=cypress-io.ghost.io#3-3-0) also contains other improvements to the networking stack.

### Corporate proxy support

In [Cypress 3.3.0](https://on.cypress.io/changelog?ref=cypress-io.ghost.io#3-3-0), support for corporate proxies was added. This allows users working at companies that require a proxy to access the outside world to use Cypress for the first time. Read the [proxy configuration documentation](http://on.cypress.io/proxy-configuration?ref=cypress-io.ghost.io) for more information on how to take advantage of this functionality.

> Check out [the GitHub issue for proxy support](https://github.com/cypress-io/cypress/issues/1469?ref=cypress-io.ghost.io) for details on what changes have been made to proxy support in 3.3.0.

### Better network error handling

Web traffic is more resilient than ever before with [Cypress 3.3.0](https://on.cypress.io/changelog?ref=cypress-io.ghost.io#3-3-0). The Cypress network layer has been updated to automatically retry HTTP requests in certain conditions where a retry is likely to succeed. This ensures that Cypress tests are less likely to spuriously fail due to network reliability issues.

If the initial TCP/IP connection fails, it will be retried up to 4 times, or until it finally succeeds.

The new retrying behavior is based off of Chrome's own network retry behavior. It was added after noticing that Chrome would retry several times to send requests that failed with `ERR_CONNECTION_RESET`, `ERR_CONNECTION_REFUSED`, or `ERR_EMPTY_RESPONSE` errors, as well as in certain other conditions. For details on exactly which retries will be made in what scenarios, check out the [pull request for automatic network retries on GitHub](https://github.com/cypress-io/cypress/pull/4015?ref=cypress-io.ghost.io).

This behavior can be disabled in [`cy.visit()`](https://on.cypress.io/visit?ref=cypress-io.ghost.io) and [`cy.request()`](https://on.cypress.io/request?ref=cypress-io.ghost.io) using the new `retryOnNetworkFailure` option. In addition, the ability to enable retries on non-200 status codes using the new `retryOnStatusCodeFailure` option has been added.

Also, previously to Cypress 3.3.0, Cypress would return an `HTTP 500 Internal Server Error` page if it hit an irrecoverable network error while making requests. Now, in Cypress 3.3.0, Cypress will immediately end the response to the browser if this happens. This will cause Chrome to display an error message that more closely matches real-world behavior. For more information, look at [the GitHub issue for changing the behavior of network failures](https://github.com/cypress-io/cypress/issues/4101?ref=cypress-io.ghost.io).

## Future network improvements

Although Cypress 3.3.0 makes massive enhancements to the Cypress network stack, there is still room for future improvement.

### Full network layer stubbing

[Full network stubbing](https://github.com/cypress-io/cypress/issues/687?ref=cypress-io.ghost.io) is something that's been on the [roadmap](https://on.cypress.io/roadmap?ref=cypress-io.ghost.io) for a while now. With full network layer stubbing, users can test _any kind of network traffic_ inside of Cypress. This will enable all sorts of functionality that would be useful for testing, including:

-   Throttling network speed to test a website under bad network conditions
-   Dynamically responding to WebSocket frames
-   Sending back dynamic content in response to requests
-   Intercepting and modifying response and request streams in flight

With the improvements made to Cypress's network layer in the 3.3.0 release, the doors are opened to add this functionality in the near future.

### HTTP/2 Support

[HTTP/2](https://http2.github.io/?ref=cypress-io.ghost.io) is much, much faster than HTTP/1.1 in most scenarios:

| Test Case | 1,000-Smurfs Load Time (ms) |
| --- | --- |
| Chrome (HTTP/1.1 only) | 4620 |
| Chrome (with HTTP/2) | 414 |

Yeah, that's right - with HTTP/2, Chrome can load 1,000-Smurfs in 414ms. That's **less than half a millisecond per Smurf**!

Currently, Cypress does not support HTTP/2 in any way, it will always use HTTP/1.1. There are a number of improvements that could be implemented:

-   Make the Cypress HTTP proxy itself use HTTP/2 support, so traffic between Chrome and Cypress can take advantage of HTTP/2 speed improvements. This would be a marginal improvement, as Chrome and Cypress communicate over `localhost` which has negligible round trip time.
-   Make Cypress use HTTP/2 for outgoing requests where possible. This is where the most potential gains would be available, but it would require re-working of Cypress's entire network layer to support HTTP/2. Additionally, we use the [`request` HTTP library](https://github.com/request/request?ref=cypress-io.ghost.io) to make outgoing requests, which has no HTTP/2 support.

[Check out this issue to see the status of HTTP/2 support in Cypress.](https://github.com/cypress-io/cypress/issues/3708?ref=cypress-io.ghost.io)

### Tracking network performance over time

The automated network performance testing described above is great, but it's difficult to act on that information - based on the time of day, which region our CI provider is operating in, and other factors, the benchmarked load times change from test-to-test. This makes it difficult to rigorously assert on the load times to really make it part of our CI process.

We would like to log the output of these performance tests to some central place, where we can look at Cypress's performance at different points in time and under different conditions. Additionally, we want to find a way to assert on this data, so we can fail a CI job if the changes from a commit cause the proxy to slow way down.