Want to move to PlanetScale? Postgres migration assistance is now available. Get started
Navigation

Blog|Tutorials

Faster PlanetScale Postgres connections with Cloudflare Hyperdrive

By Simeon Griggs |

Cloudflare recently launched Hyperdrive, which provides efficient pooling and fast queries for any MySQL or Postgres database—making optimizing how your application connects to your database incredibly easy. Once you're operating inside the Cloudflare network you get access to the full suite of features, including WebSockets—which we can use to build a real-time experience.

In this post, I'm going to outline the decisions made along the way while building a real-time application backed by PlanetScale Postgres Metal.

You may note an absence of “how to” code snippets for application code or SQL queries in this post. I’m anticipating that you’re building by describing what you want to an LLM, and so this blog post is describing what I did (and didn’t) do.

The infrastructure stack

PlanetScale Postgres Metal is the benchmarked fastest cloud Postgres, so it makes sense to pair it with the fastest cloud services. PlanetScale Metal databases are powered by blazing-fast, locally-attached NVMe SSD drives instead of network-attached storage.

Cloudflare Hyperdrive enables fast access and automated connection pooling to Postgres and MySQL databases, making it an excellent, almost zero-config way to build high-performance applications. When hosted on Cloudflare Workers you unlock access to network benefits of being inside of the Cloudflare global network, including WebSockets, which can power real-time applications in combination with Durable Objects.

That paragraph contains a lot of names of things—it's helpful to unpack each of these and understand how they intersect.

  • The Cloudflare global network is infrastructure that runs all Cloudflare services with physical locations within ~50ms of 95% of the world's internet-connected population, along with faster-than-average connections to almost every service and cloud provider.
  • Cloudflare Hyperdrive automates away the headaches of connection latency and pooling to turn your single-origin database into a globally distributed and cached source of truth. It is made of two separate yet equally important parts.
    • The edge component runs globally from within every Cloudflare data center and prepares the 7 round-trip steps of creating a connection to your database within the Cloudflare global network.
    • The connection pool is enabled physically close to your database and maintains warm connections to Postgres which are automatically given to incoming requests.
  • Cloudflare Workers is a platform for globally distributing and running your application. While it may be beneficial to run your application close to your users, performance could be improved by moving the application closer to the database through "smart placement," which we'll cover later in this post.
  • Durable Objects are stateful, single-origin request handlers for coordinating tasks. Each one lives in one data center and holds in-memory state—like a list of open WebSocket connections.
  • WebSockets are supported by Cloudflare and in this post we'll use them to coordinate real-time updates from the PlanetScale Postgres database via a Durable Object.

Hyperdrive vs direct connections

If you're interested in a demo comparing Hyperdrive vs direct connections to the database, Jilles Soeters at Cloudflare has a great demo you can check out: Globally Fast Applications with Cloudflare Hyperdrive and PlanetScale.

The frontend stack

Out of interest, I'm using React Router 7 as my front-end framework along with Tailwind CSS. I won't go into detail here because I'm not sure that these choices matter as much as they used to. I think React is an excellent choice because of the support it has for fast user interfaces.

Cloudflare has a React Router 7 starter template preconfigured for deployment to Workers.

I've historically enjoyed working with React Router 7, but while building this demo, rarely did I look at the internals of this application to see how it is written at the framework level.

Piecing the puzzle together

These are all excellent choices for building fast applications. But how and where you put these pieces together still greatly impacts the performance.

For example, if we're going to use a Durable Object to broadcast updates over WebSockets, it may be tempting to make the Durable Object the write path to the database. However, this negatively impacts performance. Durable Objects are single-threaded and hosted in a single location, making them a bad candidate for the write path.

Instead the Workers will send transactions to the database via the Hyperdrive connection.

There's also a question of when to leverage the global network or not. Globally distributing the application via Workers places it around the world, but these locations could be far from the database and the Hyperdrive connection pool—which both exist in a single origin, putting any latency between the Worker and your database. Through "smart placement" the Worker can be moved to a single location, moving latency between the user and the Worker while greatly reducing the latency to the database.

In this application I’ve chosen not to use smart placement and keep the Worker closer to every user. Monitor your own application’s performance and needs closely to see if keeping the connection between Worker and database is more useful than between Worker and user.

Finally, you need to determine whether WebSockets alone are considered a bulletproof source of real-time information in your application. In this demo, we'll leave out reconnection logic, which could help with any failed WebSocket updates. And we could have added periodic polling to run in addition to WebSockets to manually catch up any missed updates.

Essentially, we've got to draw the line somewhere. This app is going to demonstrate extreme performance, but as always your mileage may vary and how suitable this is for you will depend on the workload of your application.

Problem solving

It is one thing to build an app which returns fast queries through Hyperdrive's connection pooling and caching. I want this demo to include more.

In a nutshell, our demo app recreates the appearance of much of the functionality of a "prediction market," where users can buy options of either a "Yes" or "No" position on each market. The price of each option fluctuates based on the current volume of purchases on either position.

This functionality creates several layers of complexity which will need to leverage all of the previously mentioned Cloudflare functions.

  • Fast reads: Prediction markets are based on the outcome of real, live events. Therefore, the latency to retrieve the most up-to-date information is critical.
  • Real-time updates: Users should not have to refresh the page in order to see up-to-date information and should be able to observe changes in real-time such as trends and purchase activity across the markets.
  • Fast writes: Since the price of options can be volatile, it is important that transactions are sent with the currently correct price as well as updated in the database fast enough to broadcast updates to all other users.

Step 1: Connecting Hyperdrive

You can connect Hyperdrive to a database through the Cloudflare dashboard or CLI as well as during the creation of a PlanetScale Postgres database.

In the PlanetScale dashboard I'll select the smallest Metal database, which starts at $50/month. Though the $5/month non-Metal database could be an option for this demo too.

Once the cluster is ready I can choose to create a new role and follow the instructions to create a link between PlanetScale and Cloudflare.

The result is a connection string which I add to my application's wrangler.jsonc file

{
  // ...
  "hyperdrive": {
    "binding": "HYPERDRIVE_PLANETSCALE",
    "id": "e4fe..."
  }
}

Note that during local development reads and writes to the remote database will be slower than they are in production. You can target a database on your machine during development for faster reads and writes, but you will not experience pooling and caching optimizations.

After some LLM planning I have the scaffolding of my app to do the very basics–read and write to a database. Deployment is as simple as running a command from the terminal.

npx wrangler deploy

More terminology: What's "Wrangler?" It's the name for Cloudflare's CLI and local development environment. It is a dependency which came preinstalled with the framework template.

The alternative to Hyperdrive

We could have built an application which makes direct connections to the database primary for writes, and connections to the replicas for queries. While this can work and be optimized to be more efficient, we would have lost Hyperdrive's benefits around connection pooling and reuse, and query response caching.

Instead, with Hyperdrive we have a highly optimized database connection, just not one we had to build ourselves.

Multi-environment setup

When building there's a temptation to build everything as fast as possible using the defaults. But planning now for multiple environments will help you avoid surprises later.

Create a database branch in PlanetScale, either via the dashboard or the CLI, and configure the connection string in your wrangler.jsonc file. Add an env block to your configuration file.

{
  // ... other settings
  "env": {
    "development": {
      "hyperdrive": {
        "binding": "HYPERDRIVE_PLANETSCALE",
        // Development branch database
        "id": "84c4..."
      }
    }
  }
}

Your local development server should target this environment by setting the CLOUDFLARE_ENV environment variable.

{
  "scripts": {
    "dev": "CLOUDFLARE_ENV=development react-router dev"
    //... other scripts
  }
}

Now our deployed Worker should be targeting your main branch production database, while you target the development branch locally.

Step 2: Real-time with WebSockets

Our application simply wouldn't work if the latest data wasn't always rendered. The logic of our application should ensure at the database level that option purchases won't proceed if the purchase price as seen by the user is not a valid price at the time the transaction is written to the database.

Since the prices of options are volatile at peak times and so can change quickly, the latest data must be pushed to the browser.

This is where we need to configure a Durable Object and Cloudflare WebSockets. The Durable Object will set up the WebSocket connection to receive and send events.

In this demo, the Worker sends the transaction to the database via the Hyperdrive connection. When that transaction completes it pings the Durable Object via the WebSocket connection to fan out that update to all other connected browsers.

Reliability boundaries

One useful principle in real-time apps is to decide what is authoritative and what is just fast. In this architecture, Postgres is the source of truth and WebSockets are the low-latency notification layer.

That distinction matters. WebSocket messages can be delayed or dropped due to normal network behavior, but database writes are still durable. So the contract for clients should be: "updates are immediate most of the time, and eventually correct all of the time."

I’m using WebSockets as a fast notification channel, but not the authority. The PlanetScale Postgres database is the source of truth for all queries and transactions.

What we intentionally did not build (yet)

To keep this guide focused on principles and direction, I left out several production hardening layers which could’ve also been implemented.

  • Replay on reconnect: A reconnecting client could ask for events since a known cursor. We skipped this for simplicity.
  • Queue-backed fanout: A queue can improve durability and retries between write completion and broadcast. We skipped this to avoid extra moving parts. Cloudflare has a Queueing service, too!
  • Polling reconciliation: Periodic polling can catch any missed updates from WebSockets. We noted it earlier, but didn't implement it in the demo.

Each of these can make the system more resilient. They also increase operational complexity, and for this demo I chose to show the fastest path to a working real-time architecture first.

Step 3: Increasing load

We now have a working application with support for multiple environments, transactions and real-time updates. But we're not building a toy, we want to prove this application can handle some scale. Many users, a lot of transactions, and a lot of markets.

To simulate load, I've used a seed script that creates fake users and markets, then fans out a large volume of initial transactions across those markets.

Then in the application I have a Start Trading button that enables a client-side simulator that continuously posts trades at a fixed cadence to random open markets. Each request carries expected price/version data and slippage tolerance, so the backend can reject stale quotes while successful writes immediately broadcast over WebSockets to every connected browser.

This isn't a replacement for faking traffic from around the world, but it does give us a good sense of what it looks like to create many transactions in one tab with another tab open, which is receiving the same live updates. Because of the way we have configured the real-time updates, the user that sends the transaction will get their feedback faster than users who are only listening. It's a small trade-off I'm considering acceptable in this demo. See the earlier notes about making real-time more robust through queues and polling.

Monitoring PlanetScale performance

Cloudflare Hyperdrive made handling connections and requests fast and easy, but there's still plenty we can do on the PlanetScale side to improve performance.

Query Insights can help you understand how your database is performing under load. As soon as we've simulated traffic, there may be some insights for our queries to be improved. You can access these insights from the PlanetScale dashboard or the CLI.

Or better yet, with the PlanetScale MCP server, you can just ask your LLM. It has the capability of reading insights data and would then be able to make improvements or act on recommendations such as creating indexes.

Implementing database best practices

It's extremely likely that your application has a lot of AI-authored code. But this doesn't mean it has to be bad. PlanetScale offers a package of agent skills in order to train your LLM in best practices as PlanetScale sees them when it comes to data structures and writing queries. Make it a habit to routinely audit your application against these skills--before your application is a runaway success.

Increasing capacity

Should your application be so successful as to overwhelm your current cluster size, you can resize your cluster at any time through the PlanetScale dashboard or CLI. Increase CPU, memory, storage or add more replicas as needed.

You can get started with Cloudflare Workers, WebSockets, Durable Objects and Hyperdrive for free. See their documentation for resource limits and upgrade options.

Sharding Durable Objects

A lot of what I built for this demonstration is about global distribution. However, I'm still relying on a single Durable Object to send WebSocket updates to all users. Cloudflare's own documentation gives guidance that you can scale Durable Objects horizontally—sharded with a key—should you come close to exhausting their allocated resources.

Conclusion

This demo shows how to build a real-time application with Cloudflare and PlanetScale. It covers the basics of connecting to the database, setting up real-time updates, and simulating load. It also covers some of the best practices for building a real-time application, such as using a Durable Object to coordinate real-time updates and using a WebSocket to broadcast updates to all connected browsers.

As always, you may need to adjust the approaches in this demo for your own application, and introduce some additional features to make your application even more robust. But as you can see, the time to a quite complicated proof-of-concept which is relatively sound has never been lower and all built on a stack that's ready to scale.