The case against Urql when Apollo Client costs nothing extra

Last updated: May 28, 2026

Urql vs Apollo Client comes down to one decision in 2025: pick Apollo Client for any production React app with mutations and shared data. Apollo Client’s core is MIT-licensed and free, ships useSuspenseQuery, useFragment, and useBackgroundQuery since version 3.8, and normalizes its cache by object identity so a single mutation updates every component that references that entity. Urql’s default document cache only invalidates by query string plus variables, so cross-component updates require installing graphcache or manual refetches — work that erases the bundle advantage that justified picking Urql in the first place.

Decision point
The bundle is a decoy.
At first glance, Urql looks like the smaller, simpler, lower-cost alternative to Apollo Client. The hidden turn is that Apollo Client is already MIT-licensed and tree-shakable, while Urql’s “lighter” defaults push the hard problems — normalized caching, auth refresh, cross-component invalidation — into your application code. Choose the wrong client this quarter and you buy months of cache-invalidation patches plus a migration nobody scheduled.
If Apollo Client itself is free, what is Urql’s “lightness” actually saving you in 2025?
Bundle mythCache trapPick Apollo
Apollo Client is already MIT-licensed and tree-shakable, and Urql’s ‘lighter’ defaults push the hard problems (normalized caching, auth refresh, cross-component invalidation); the rest of the decision follows from that.

In urql vs apollo client, the real cost is not the bundle but the cache work you defer — Urql’s document cache hides invalidation bugs that surface the moment mutations touch sibling components. For almost every new React project choosing a GraphQL client now, the answer is Apollo Client: its core is MIT-licensed, its modern React hooks are documented in the Apollo Client 3.8.0 release notes, and the old Urql advantages are now too narrow to outweigh normalized caching and ecosystem depth.

  • Apollo Client core is MIT-licensed; the paid GraphOS products are server-side and optional.
  • Apollo Client 3.7 added @defer support according to the Apollo Client v3 changelog; Apollo Client 3.8 added useSuspenseQuery, useFragment, and useBackgroundQuery in the 3.8.0 release notes.
  • Urql’s default document cache hashes query + variables, so cross-query updates need either graphcache or manual refetches, as the official Urql document caching docs explain.
  • Urql does not expose a one-call equivalent to Apollo’s documented client.resetStore(); see the canonical urql issue #297.
  • In the reproducible build shown below, the gzipped delta between a fully-featured Apollo setup and a fully-featured Urql setup with graphcache and auth exchange is small enough that it should be treated as a local measurement, not a universal benchmark.

Start with the cache model, because it explains why the “smaller client” argument often collapses in production.

Topic diagram for The case against Urql when Apollo Client costs nothing extra
Purpose-built diagram for this article — The case against Urql when Apollo Client costs nothing extra.

The verdict: Apollo Client is free, and that changes the math

The classic argument for Urql was never really about bytes. It was about the perception that Apollo Client was a heavyweight commercial library you tolerated because GraphQL needed a client. That framing made sense in older comparisons, when Apollo Studio dominated the conversation and the company’s monetization strategy was easier to confuse with the client package. It does not survive current source checks. The @apollo/client package is MIT-licensed in the canonical repo; Apollo’s revenue lives in GraphOS, Federation routing, and managed schema tooling, which are server-side products you can ignore entirely.

Once you accept that Apollo Client itself is free, the only honest reasons to prefer Urql are technical: smaller bundle, simpler cache model, lighter API surface. Each of those reasons has a current answer that differs from the one older comparison posts keep recycling.

The stale snapshot Urql comparisons still run on

Current Urql-vs-Apollo posts often cite older comparison articles, screenshots of issue counts, and bundle-size numbers from package-size tools. Each one has aged badly.

Older comparison posts were written before Apollo Client 3.7, 3.8, and 3.9 shipped — meaning suspense hooks, fragment hooks, @defer support, and the removeTypenameFromVariables link were not part of the comparison. Apollo’s v3 changelog and the 3.8.0 release notes document that gap directly.

The GitHub issue count comparison measures user volume and triage policy, not code quality. Apollo Client appears to have a larger public ecosystem and higher issue volume than Urql, so its issue tracker likely carries more traffic for that reason alone — treat the count as a popularity signal, not a defect signal. Citing it as a quality measure is the same kind of error as benchmarking a popular database by counting Stack Overflow questions.

Bundle-size numbers are usually quoted without a denominator. “Urql ships smaller than Apollo” sounds decisive until you compare a production-equivalent setup and remember the rest of the application JavaScript, images, analytics, and UI library imports around it. Bundle math matters; it just does not matter in the way old comparison posts imply.

Bundle size now: the measured number, not the spec sheet

Install both clients in a fresh Vite + React app, configure the realistic feature set each one needs to be useful in production, and measure the gzipped delta. For Apollo that means @apollo/client with InMemoryCache, error link, and an auth link from the documented Apollo Link system. For Urql that means urql, @urql/exchange-graphcache, and auth exchange behavior from Urql’s official exchange model, because the Urql you compare against Apollo is the one that does what Apollo does, not the bare document-cache version.

Terminal output for The case against Urql when Apollo Client costs nothing extra
Output captured from a live run.

The terminal output above shows the result a reader can reproduce: once Urql is configured with a normalized cache and auth refresh, the gzipped bundle gap shrinks to a local delta that is small compared with ordinary app-level JavaScript budgets. Choosing your GraphQL client primarily to save that local delta is the wrong order of optimization when a single careless UI-library import can erase the gain.

The document-cache trap: Urql’s simplest feature is its biggest deferred cost

The most-praised Urql feature — its default document cache — caches by the hash of the GraphQL query string plus its variables. That is genuinely simple, and for read-only dashboards it is enough.

It breaks the moment two components depend on overlapping data. Render a paginated list in one component and a “recently updated” sidebar in another, both pulling the same items by different queries. Mutate an item in the list. Apollo Client’s normalized cache, documented around object identity and InMemoryCache, can update both views when they reference the same entity. Urql’s document cache treats the two queries as unrelated strings unless you add graphcache, an updates resolver, or a manual refetch, as the official Urql document caching docs describe.

The diagram above captures the pattern. As soon as you have a mid-size app — anything past the second screen with mutations — you install @urql/exchange-graphcache, and at that moment you are running a normalized cache with a thinner API surface than Apollo’s, fewer docs, and a public issue (urql-graphql/urql#297) showing there is still no one-line equivalent of client.resetStore(). Apollo’s resetStore documentation describes the helper alongside refetch-on-reset behavior — a single concept, one method call.

The simpler-cache pitch is a deferred cost, not a saved one. You either pay for normalization on day one with Apollo or pay for it later with Urql, in a sprint nobody scheduled.

What Apollo shipped after the old comparisons

The features that frozen comparisons miss are the ones a modern React app actually wants. Concretely, since the older comparison cycle, the Apollo Client 3.8 release notes document useSuspenseQuery, useFragment, and useBackgroundQuery. The Apollo Client v3 changelog documents @defer support in the 3.7 line and additional query-reference APIs in the 3.9 line. The removeTypenameFromVariables link removed a long-standing footgun around mutation input objects.

Reactive variables — Apollo’s lightweight client-state primitive — are documented in the official Apollo reactive variables guide and are a credible Redux replacement for data that does not belong in the GraphQL cache. Urql can implement comparable behavior through custom exchanges and local state patterns, but “you can write your own” is exactly the deferred cost the Urql pitch denies.

Where Urql still wins — and why those cases are narrower than they look

Two scenarios remain where Urql is the better pick. The first is a Svelte, Vue, or Solid project where you want a GraphQL client whose React tie-in is not the design center; Urql’s official docs describe framework bindings through its core client and exchange architecture. The second is a small read-only dashboard with no mutations and no overlapping queries, where the document cache will never hit its failure mode and the smaller default surface is real upside.

Everything else — App Router, React Native, schema-federated backends, anything with auth refresh or pagination plus mutations — is the long tail where Apollo’s normalized cache, codegen ecosystem (graphql-codegen’s typescript-react-apollo plugin), and devtools pay back the install size within a sprint.

A real decision rubric for urql vs apollo client

Radar chart: Urql vs Apollo

Multi-metric comparison — Urql vs Apollo.

The radar chart above scores both clients across the dimensions that actually matter for picking one now — normalized cache ergonomics, suspense support, ecosystem and codegen, bundle size, and framework breadth. Use the table below to translate that into a project signal.

Decision rubric — which GraphQL client matches your project signal
Project signal Pick Why
Next.js App Router or React Server Components Apollo Client Apollo documents React suspense hooks in the 3.8.0 release notes; Urql’s RSC story is not as first-party.
Mutations that touch lists rendered elsewhere Apollo Client Apollo’s normalized cache model is built around shared object identity; Urql’s default document cache is query-document scoped.
Schema federation or large team Apollo Client Codegen plugins, devtools, and managed schema tooling are first-class in the Apollo ecosystem.
Auth-refresh races with concurrent queries Apollo Client Apollo’s link system has well-documented auth and error handling patterns; Urql can do this through exchanges, but the exchange logic is app-owned.
React Native app with offline persistence Apollo Client Apollo’s normalized cache and persistence ecosystem make the offline path more established for React-first teams.
Read-only Svelte or Vue dashboard, no mutations Urql Smaller default surface, framework-agnostic binding, and a document cache that is sufficient when no invalidation occurs.
Library author shipping a GraphQL-powered widget Urql Lighter peer-dependency footprint when you cannot assume the host app already has Apollo.

The migration tax nobody prices in

“Should we switch to Urql?” threads usually estimate the work as “swap the provider and the hooks.” That is the cheapest part. The expensive parts are rewriting cache update logic for every mutation, replacing refetchQueries with graphcache updates resolvers, porting custom links to exchanges, and migrating codegen output from typescript-react-apollo to typescript-urql. For a medium codebase, that is likely multiple engineer-weeks. The bundle savings shown in the measurement above will not pay for that migration inside a normal product cycle.

The strongest counter-argument

The honest case for Urql is the one its maintainers actually make: a small, composable core where every piece of behavior — auth, retry, dedupe, caching — is a swappable exchange. If you are the kind of team that wants to read every line of code that runs on a network request, Urql’s architecture is genuinely cleaner than Apollo’s link chain, and the Urql architecture docs document this design philosophy clearly.

The rebuttal is that this advantage matters only when you both want that level of control and are willing to pay for it in writing — and rewriting — exchanges as your app grows. For most product teams shipping features, “fewer abstractions to learn” is a smaller win than “a normalized cache that works the first time.” Apollo’s link API is harder to admire, and easier to ship with.

What the sources prove

This source check verified each claim against the official documentation, project changelogs, and canonical GitHub issues — not against secondary blog posts. The Apollo Client feature claims come from the Apollo Client v3 changelog and 3.8.0 release notes. The Urql document-cache mechanism and exchange architecture come from the official Nearform-hosted document caching docs and architecture docs. The resetStore gap is verified against urql issue #297 and Apollo’s cache reset docs side by side. The MIT-license claim is verified directly against the LICENSE file in apollographql/apollo-client. Bundle figures are the measured gzipped output of a fresh Vite + React install of each client with feature-equivalent exchanges configured, shown in the terminal artifact above.

If you are picking a GraphQL client this quarter, install Apollo Client and move on. The bundle delta will not save your app, the simpler cache will not stay simple, and the migration tax later will cost more than the install savings now. Save Urql for the narrow cases — Svelte-first apps, library authors, read-only dashboards — where the architecture genuinely fits.

References