Free-Tier Limits Made My Architecture Better

Published: 2026-07-06

Most architecture advice is written from a position of abundance: choose the right tool for the job, scale when you need to, add Redis if the database can’t keep up. It’s good advice, as far as it goes. It also assumes you have options.

I didn’t have those options when I built pulseticker, a real-time stock dashboard I ran entirely on free tiers. Every infrastructure decision had a hard edge: this API gives you X requests per month, this service allows Y concurrent connections, this platform just stops here. There was no “I’ll upgrade when it gets slow” — upgrading wasn’t on the table.

If you’ve ever built something on a shoestring, you know the feeling: when there’s no paid tier to fall back on, every choice gets made twice as carefully, because a wrong guess has nowhere to hide. What surprised me, looking back, is how often that pressure produced a better decision than I’d have made with room to spare.


Decision 1: Finnhub over Twelve Data

A real-time dashboard needs real-time data. Financial data APIs are, as a category, expensive — most gate WebSocket access behind paid plans entirely.

I evaluated two providers with workable free tiers:

Twelve Data has broader exchange coverage, clean documentation, and a clear SLA. Its free tier includes REST endpoints and a trial WebSocket connection. The operative word is trial — it expires, and there is no persistent free WebSocket tier.

Finnhub includes WebSocket streaming for up to 50 symbols (US stocks + Forex) on the permanent free tier. No credit card required. No trial expiry.

The constraint made the decision obvious. Twelve Data’s broader coverage did not matter if the connection I needed would stop working. Finnhub’s narrower scope was a real limit I could build around.

This kind of decision is easy to second-guess when you have no constraint. “Twelve Data has better ASX coverage, maybe I’ll need it later.” A real limit eliminates the hypothetical and forces a commitment.

Twelve Data came back anyway

That commitment didn’t stay clean. Not long after, I noticed that chart history wasn’t working: clicking a symbol drew a live line with nothing behind it — no history, just the line starting from whatever tick happened to arrive first. I traced it back to the historical candle endpoint I was relying on for chart backfill: Finnhub’s free plan didn’t actually include it, and I hadn’t caught that until I saw the empty results on screen.

The live WebSocket feed was unaffected — free-tier access there was real. It was only the historical lookup that had never been covered. So I brought Twelve Data back, not for streaming, but for exactly the one thing it’s good at on a free plan: REST-based historical candle data, merged with Finnhub’s live ticks at the edge where history ends and the line keeps drawing. Finnhub still owns the WebSocket. Twelve Data owns the past. I’d just missed the Finnhub gap the first time around.


Decision 2: Letting Go of ASX

I am based in Sydney. I genuinely wanted to support ASX (Australian Securities Exchange) tickers — it would have made the project feel less abstract.

ASX data sits behind a paid plan on every provider I evaluated. Finnhub, Twelve Data, and others all treat Australian market data as a premium feature. I briefly looked at Yahoo Finance’s unofficial API as a workaround, but it has no WebSocket support and no stability guarantee. It is not a maintained API; it can break without notice.

So I let it go.

Instead, I leaned into Forex pairs — AUD/USD and AUD/JPY — which Finnhub does include in its free WebSocket tier. It is a smaller gesture than full ASX support, but it kept the Australia-relevant angle without building on a foundation that could collapse.

This was a harder decision to make than the Finnhub/Twelve Data one, because it meant accepting a real limitation of the project. But the alternative — building on Yahoo Finance’s unofficial API — would have meant shipping something unreliable. The constraint protected the demo from a worse outcome.


Decision 3: Graphile Worker over Redis

The alert system originally used BullMQ and Upstash Redis. Upstash’s free tier allows 500,000 commands per month.

I burned through that in a few days of development, because the gateway was writing a queue job for every incoming price tick — dozens per second, regardless of whether any alert condition was anywhere close to being triggered.

The full story is in a separate post, but the short version: the real fix was a pre-check cache that prevents unnecessary jobs from being created. Graphile Worker — which uses the existing PostgreSQL database as its queue backend — removed Redis from the stack entirely.

One less service. One less free-tier quota to track. One less connection string.

The constraint (Upstash’s limit) forced me to think carefully about when a job should actually be created, which was the right question to ask from the beginning. Without the constraint, I might have just upgraded to a paid tier, called it fixed, and never gone back to look at why the code was doing ten times more work than necessary.


Decision 4: The Pre-Login Demo Cache

Most SaaS landing pages show a screenshot or a recorded demo. I wanted pulseticker to show live, moving data before a user creates an account — a fixed watchlist (AAPL, MSFT, VOO, AUD/USD) that updates in real time.

The naive implementation: connect every visitor to the Finnhub WebSocket and stream prices directly. The problem: Finnhub’s free tier allows one WebSocket connection. Two visitors simultaneously would require two connections. The demo would break under any real load.

The solution: a single background process on the server that maintains one Finnhub connection, caches the latest prices in memory, and refreshes a server-side snapshot every 10 seconds. The pre-login endpoint returns that snapshot — one API call serves any number of concurrent visitors.

Here’s what that ends up looking like — smaller than you might expect, which is sort of the point:

// Simplified — the server maintains one connection and one cache
class PriceSnapshotService {
  private snapshot: Record<string, number> = {};

  constructor(private finnhubGateway: FinnhubGateway) {
    setInterval(() => this.refreshSnapshot(), 10_000);
  }

  private async refreshSnapshot() {
    this.snapshot = this.finnhubGateway.getLatestPrices(DEMO_SYMBOLS);
  }

  getSnapshot() {
    return this.snapshot;
  }
}

The 10-second interval is not a technical constraint — it is a deliberate choice shaped by the rate limit. Live users with an account get genuine WebSocket streaming. Pre-login visitors get a snapshot that is fresh enough to look real without burning through connection limits.

This is a cleaner architecture than “stream everything to everyone.” It separates the concerns: authenticated streaming for users who need it, cached snapshots for visitors who do not. The constraint motivated the separation; the separation is genuinely correct regardless of whether the constraint exists.


The Pattern

Looking back at all four decisions, the pattern holds up:

The constraint eliminated options that looked attractive but were fragile — Yahoo Finance’s unofficial API, Upstash’s temporary connection, an architecture that made redundant API calls. Designing around real limits meant committing to choices that were durable by necessity, not by intention.

Unconstrained design tends toward optionality: “I could use Redis, or Postgres, or maybe both, let’s see what I need.” Real constraints force the question earlier: “what do I actually need, given what I actually have?” The answer is usually simpler than the one you’d have talked yourself into otherwise.

None of these decisions were clever, and I don’t think I’m smarter for having made them. They were just decisions made in response to real edges instead of hypothetical ones — and that alone got me to architecture that’s easier to explain, easier to maintain, and cheaper to run than anything I’d have built with a credit card and an assumption of unlimited resources.

pulseticker still runs entirely on free tiers. It hasn’t cost me a cent since it launched, and honestly, I’m not in a rush to change that. You can see the pre-login demo cache in action at pulseticker.vercel.app.


Further Reading

  • Finnhub API docs — WebSocket — the free tier WebSocket reference, including symbol limits and supported exchanges.
  • Graphile Worker — the PostgreSQL-backed job queue used in this project. The README explains the design trade-offs clearly.
  • Indie Hackers — a community of founders discussing building profitable products under real resource constraints, not just cost management.