If you've ever wondered why your chart updates feel sluggish compared to what you see in BookMap or a professional terminal, the answer is probably your data feed. Not your computer, not your internet connection. The feed.
I got curious about exactly how big the gap is between different feed tiers, so I built a test. Four data sources for SPY, all running at the same time on the same server, outputting to a common CSV format so I could compare them trade by trade.
The results were more spread out than I expected.
The Setup
The server is a DigitalOcean droplet in North Bergen, New Jersey — which puts it a few miles from NASDAQ's matching engine in Carteret and NYSE's in Mahwah. Geography matters here, and I'll come back to why.
The four feeds I tested:
-
dxFeed Direct — native binary push
protocol, connecting to
barolo.dxfeed.com:7930(Oracle Cloud). This is the same feed BookMap uses. - TOS Internal — tapped from inside ThinkorSwim's JVM using a JVMTI agent. More on this below.
- Schwab WebSocket Streamer — Schwab's official streaming API, pushing LEVELONE_EQUITIES events over WebSocket.
-
Schwab REST API — polling
/marketdata/v1/quotesevery 200ms.
All four writing to matching CSV files with timestamps, so I can do post-hoc comparison on the same ticks.
The Numbers
Before getting into latency estimates, here's what I actually measured from TCP connect times:
| Feed | TCP Connect | Infrastructure |
|---|---|---|
| dxFeed Direct | 9ms | Oracle Cloud, same metro as droplet |
| TOS Internal (Schwab) | 59ms | Schwab HQ, San Francisco (162.93.113.30) |
| Schwab WebSocket | ~59ms | Same Schwab SF infrastructure |
| Schwab REST API | 3ms | Akamai CDN, Secaucus NJ (nearby) |
That last one looks great at 3ms. Don't get excited.
The Schwab REST API goes through Akamai's CDN edge in Secaucus, which is close. But the CDN is just the edge node. The actual quote data still has to come from Schwab's backend in San Francisco. And more importantly, you're polling it. The measured HTTP round-trip was 228ms mean, ranging from 186ms to 398ms. Combined with the 200ms polling interval, you're looking at roughly 400ms of effective latency before you even process a quote.
That's not a feed. That's a postcard.
Ranking all four by expected end-to-end latency:
| Rank | Feed | Expected Latency | Protocol |
|---|---|---|---|
| 1st | dxFeed Direct | ~10ms | QDS binary push over TCP |
| 2nd | TOS Internal | ~50ms | QDS binary push over TLS |
| 3rd | Schwab WebSocket | ~100–200ms | WebSocket JSON push |
| 4th | Schwab REST | ~400ms | HTTPS/JSON polling |
That's roughly a 40x spread from best to worst.
Why dxFeed Is So Much Faster
Two reasons: protocol and geography.
dxFeed uses QDS — a binary push protocol that's event-driven and compact. No JSON parsing, no HTTP overhead, no polling. The server pushes data the moment a trade happens. It connects to Oracle Cloud infrastructure that, for my New Jersey droplet, is in the same metro area. That's why TCP connect is 9ms.
The TOS Internal feed uses the same QDS protocol. The gap between 1st and 2nd place (10ms vs 50ms) isn't the protocol — it's Schwab's data path routing everything through San Francisco first. Cross-country round trip adds roughly 40ms of baseline latency you can't engineer away without moving your server to the West Coast.
The network path looks like this:
Exchange → dxFeed (Oracle Cloud, NJ) → Droplet [~10ms]
Exchange → Schwab (SF) → TOS on Droplet [~50ms]
Exchange → Schwab → WebSocket Streamer → Droplet [~100-200ms]
Exchange → Schwab → REST API → Akamai → Droplet [~400ms]
The TOS Tap: A JVMTI Agent
This is the part I found most interesting to build.
ThinkorSwim runs on the JVM. It has its own internal DXFeed subscription that it uses to power all those charts and level 1 quotes you see in the UI. That subscription is sitting there, receiving real-time data, completely separate from Schwab's official API.
The way to get at it: inject a JVMTI agent into the running TOS JVM.
JVMTI (JVM Tool Interface) is the API Java uses for debuggers and
profilers. When you attach a native agent to a running JVM, it
gets access to the full JVM internals — live classes, live
objects, everything. The trick is that TOS loads its DXFeed
classes through a custom DLLClassLoader that's
isolated from the standard classloader hierarchy. You can't just
do a normal Class.forName() and find the DXFeed
instance.
So the agent uses reflection to walk the classloader tree, find TOS's isolated classloader, load the DXFeed API classes through it, and subscribe to the same feed TOS is already receiving. It's not intercepting TOS's data stream — it's subscribing alongside it, using TOS's entitlement credentials.
Once you're subscribed, the data comes through at the same speed TOS sees it. Which, as measured above, is about 50ms — not because the protocol is slow, but because Schwab routes it through San Francisco.
Is this a reasonable approach? Probably not for most people. But it works, and the data quality is identical to what TOS shows you.
Schwab's Official APIs
Schwab offers two legitimate paths for real-time data.
The WebSocket streamer is push-based, which is the right idea. It
pushes LEVELONE_EQUITIES events when quotes change. The catch is
authentication: it requires a full OAuth
authorization_code flow, meaning a browser redirect
and a user clicking "Authorize." You can't run this headless
without jumping through extra hoops. Once you're connected and
authenticated, the data is reasonably fast — probably
100-200ms, with Schwab's SF infrastructure as the bottleneck.
The REST API uses client_credentials OAuth — no
browser, no interactive login, just your client ID and secret.
It's by far the easiest to set up. And it's genuinely fine for
things that don't need fast data: end-of-day analysis, portfolio
valuation, checking a quote before you call your broker. For
anything where you care about the last 200ms, it won't help you.
Polling is a fundamental constraint, not an implementation detail.
You can poll faster, but you pay in API rate limits.
What This Means in Practice
If you're watching SPY charts in ThinkorSwim and wondering why your entries feel slightly off from what you intended — part of that is Schwab's data path adding 50ms of geography tax. It's not a huge deal for most trading, but it's real.
If you're pulling quotes from the Schwab REST API and trying to trade off them, you're working with data that's 400ms stale by the time you process it. For a volatile underlying on a fast-moving tape, that can be several ticks.
dxFeed Direct solves both problems. It's geographically close (for a NJ server), the protocol is lean, and it's push-based. The downside is that it requires an entitlement — a BookMap subscription gets you one, or you can license it directly from Devexperts. It's not a free feed.
For this project, all four feeds writing to the same CSV format means I can see exactly how much signal each one is losing. So far, the REST API is losing the most — not surprising — and dxFeed is the reference I'm measuring everything else against.
Verification: Three Scheduled Runs
The numbers above come from TCP connect times and protocol analysis. To get real tick-by-tick latency comparisons under different market conditions, I have three benchmark runs scheduled:
- Tuesday May 26 at 12:00 PM ET — midday, steady volume. The baseline reading.
- Tuesday May 26 at 3:55 PM ET — five minutes before close, high volume. This is where feed differences should be most visible.
- Wednesday May 28 at 9:35 AM ET — five minutes after market open, burst activity. The tape moves fast here and slow feeds drop ticks.
Each run captures 10 minutes of SPY data across all four feeds simultaneously. Same droplet, same CSV format, same clock. After the three runs I'll have latency distributions across calm, volatile, and burst conditions — not just a single snapshot.
I'll update this post (or write a follow-up) once the data is in.
I'm a software developer who trades on the side. This is not financial or trading advice — just a write-up of a side project. Latency figures are measured from a specific server in a specific location; your numbers will differ. Also: tapping the TOS JVM the way I described is something I do on my own machine for research purposes. Whether Schwab's ToS allows it is a different question, and I haven't read it closely enough to have a confident answer.
Enjoyed this post?
Get notified when I publish something new. No spam, unsubscribe anytime.