Insights Case Study
Case Study

What the Free Tier Actually Costs

23 July 202623 Jul 2026 12 min read Infrastructure · Platform Engineering · Graph Databases · Cost
Summary

Rippli spent a year climbing down the managed-hosting ladder, Replit, then Render's free tier, then a bare VPS, because the workload was the wrong shape for free infrastructure, not the wrong size. This is the migration in full: what broke at each rung, what the move actually bought, the security regression we introduced on the way, and how to tell whether your own workload belongs on a platform or on a machine.

Summary. Rippli spent a year climbing down the managed-hosting ladder, a browser IDE, then a free cloud tier, then a rented machine, because the workload was the wrong shape for free infrastructure, not the wrong size. This is the migration in full: what broke at each rung, what the move actually bought, the security regression we introduced on the way, and how to tell whether your own workload belongs on a platform or on a machine.

#The wrong question

The question teams ask about free hosting is "will we outgrow it?", as if the ceiling were traffic, and the answer were a date.

Rippli never got close to a traffic ceiling. A handful of concurrent users was enough to take the API down.

What it does explains why. Rippli reads live financial news, extracts the companies named in it, and maps how those companies are connected, so that when something happens to one of them, you can see the shock travel. A fire at a component plant is not a story about that plant. It is a story about the three assemblers who buy from it, the two carmakers those assemblers supply, and the index those carmakers sit inside. Most tools will tell you the plant burned. The question worth money is who else just had a bad morning, and how long it takes to show up in their numbers.

Answering that is a traversal, not a lookup. And a traversal has a shape that free infrastructure is not built for. A single request fans out into twenty article fetches, builds dataframes over them, runs NLP extraction, computes sentiment, and writes into a graph, all inside one synchronous HTTP handler. That shape is memory-bound, stateful and multi-process. Free tiers are priced for work that is I/O-bound, stateless and single-process. No amount of staying small fixes a mismatch like that.

You do not outgrow the free tier. You discover you were never the customer it was designed for.

#What broke, rung by rung

Era Where it ran What the user saw The reflex Actual root cause
Genesis Browser IDE Process dies mid-search; work lost between sessions Trim dependencies, restart, try again One environment asked to be an IDE, a runtime and a database at once
Free cloud Free web service + managed graph DB 502 on any broad query; 12–25s first-request delay; a hard node ceiling Add a retry, blame the network, consider a bigger plan Heavy synchronous compute inside a 512MB HTTP process
Owned metal Rented VPS, containers, one compose file Deploys that only one laptop could perform Write a script for it We bought compute and paid for it in operations

Read the last column downward and the story is not "we needed a bigger box." Each environment was solving a different problem than the one we had, and the mismatch surfaced as a different symptom every time. The reflex column is the more useful one to sit with: every one of those reflexes is what a competent engineer reaches for first, and not one of them touches the actual cause.

#Memory ceilings do not degrade, they terminate

The free tier gives you 512MB of RAM. A search for something broad, a large-cap name with wide coverage, would fetch twenty articles, build dataframes over them, and load an NLP model into the same process. The worker hit OOM and died. The user got a 502.

The instructive part is what a 502 teaches you, which is nothing. It does not say memory. It does not name the query that killed it. It is indistinguishable from a network blip. We spent real time chasing it as intermittent connectivity before accepting that the process was being executed, reliably and on schedule, by the kernel.

A CPU ceiling is legible: things get slow, you watch latency climb, you go looking. A memory ceiling produces a healthy process, then no process. There is no gradient to follow. That is why OOM gets filed under flakiness for weeks at a time, not because engineers are careless, but because the failure withholds the one signal debugging depends on.

Worth noting which queries died. Not the obscure ones. The broad, well-covered, obviously-interesting names, the ones a new user types first because they want to see whether the product knows anything. The failure was perfectly correlated with the value of the question being asked.

#A cold start is an acquisition cost, not a performance cost

Free instances spin down when idle. The first search of the day paid 12 to 25 seconds before the query even began.

The temptation is to file this under performance and put it behind the features. It doesn't belong there. Cold starts land entirely on first-time visitors and on anyone returning after a gap, precisely the two groups whose opinion of the product is still forming. Your loyal daily user never sees it. Your evaluator sees nothing else.

Once you move that number out of the performance budget and into the funnel, the arithmetic changes completely, because you are no longer comparing it against other latency work. You are comparing it against everything else you spend to get someone to try the product once.

#Node limits and relationship limits are different currencies

The managed graph tier caps you at roughly 50,000 nodes and relationships combined. That sounds generous right up until you remember what a relationship mapper does: it grows combinatorially. Every new entity is not one row. It is potentially an edge to everything already in the graph.

This is the part where the billing model and the product disagree. You budget in nodes, because entities are what you can count in advance. You run out in edges, because edges are the thing you are actually building. And the edges are the asset, a graph of ten thousand companies with no relationships between them is a list, and lists do not tell you who else just had a bad morning. The ceiling we hit was not a storage limit. It was a limit on how much of the product could exist.

Underneath it sat a slower tax. Every graph query crossed a network boundary from the application to the managed database. Individually trivial, a few milliseconds. But a ripple traversal is not one query, it is a fan of them, and per-hop latency multiplies through a traversal in a way it never does through a REST endpoint. The same round trip that is invisible in a request/response app becomes the dominant cost in a graph walk, and it puts a hard commercial floor under how deep a user is allowed to look.

#The mistake that predated every hosting decision

The genuine error came before any of it: we modelled a graph problem in whatever store happened to be nearest.

Rippli's core object is (Company)-[:SUPPLIES]->(Company). Expressed in a relational store, that is a join table, and every question worth asking, what sits three hops upstream of this supplier?, becomes a recursive query that gets slower exactly as the data gets more valuable. Expressed in JSON caches, it becomes application code doing traversal by hand, badly.

We spent months treating that as a performance problem to be tuned. It was a modelling problem, and it stayed unsolved on every platform we tried, because a platform migration cannot fix a data model. Moving to a real graph database was the single change that made the rest of the architecture obvious.

If the same bottleneck follows you across three environments, stop migrating. The bottleneck is not the environment.

#What owning the machine actually bought

We rented a VPS and moved everything onto it in containers behind a single compose file: the API, a relational store, a cache, and a dedicated graph instance. The managed graph tier was retired entirely. The graph is now well past five figures of records with no pricing conversation attached to it.

Three wins, and only one of them was the compute.

Colocation removed a whole latency class. With the graph on the same host as the API, traversals stopped being network calls. The multi-hop walk that was uncomfortable across a cloud boundary became unremarkable across a loopback interface. In product terms: the depth a user is allowed to explore stopped being a function of what the traversal costs us. That is the difference between a tool that shows you a company's direct suppliers and one that shows you the exposure you didn't know you had.

One compose file made the orchestration honest. Four services in a single definition that runs identically on a laptop and on the server. The free-tier architecture had been four vendors, four dashboards and four failure modes that could not be reproduced locally, which meant every production bug started with a guess.

The pipeline became expressible. Once every dependency was one hop away, we could write the thing the way we had always drawn it on a whiteboard: one context object threaded through stages, each stage enriching it.

class SearchPipeline:
    STAGES = (...)                    # each stage: Context -> Context

    def run(self, query: str) -> Context:
        ctx = Context(query=query)
        for stage in self.STAGES:
            ctx = stage(ctx)          # each enriches; none of them fetch
        return ctx

What that shape buys is specific. Stages are unit-testable in isolation, because none of them own a connection. Stages are reorderable, because the contract between them is one object. And adding an enrichment step is a local change rather than a new branch through a request handler, which matters when the roadmap is largely a list of new things to infer about an entity. None of that was permitted by the VPS in any technical sense. It was permitted because we stopped writing defensive code around four network boundaries, and the shape underneath became visible.

#The part where we made things worse

Owning the machine cost us the one thing the managed platform was genuinely excellent at: push to deploy. In its place we had SSH, a pull, and a rebuild, run by hand.

So we automated it badly. Sixty-plus Expect scripts, each wrapping an SSH session, several of them carrying credentials in plain text.

Every one of those scripts was individually reasonable. Each solved a real annoyance in about five minutes. Nobody ever decided to build a credential-leaking deployment system, it accreted, one small justified file at a time, which is how most security incidents are actually authored.

The fix, when we finally did it properly, was smaller than what it replaced: key-based SSH with passwords out of the loop entirely, and two short shell scripts under twenty lines each. Sixty files deleted in one sweep.

The lesson is about the interval, not the fix. The gap between the platform stopped doing this for us and we replaced it deliberately is where the damage happens, and it is invisible while it is happening, because each individual improvisation is defensible. If you take on operations, take on the deployment path in the same week, not after it has been improvised sixty times.

#What changed when the agents arrived

This connects directly to the ops cost, so it is worth being precise about what actually moved.

Working through an agentic IDE changed the maintenance economics far more than the writing economics. The SSH key migration, the sixty-script deletion, the several-hundred-megabyte virtual environment that had been committed years earlier, the dependency-directory prune, the ignore-file standardisation, this is exactly the work that never gets prioritised, because each item is individually too small to schedule and collectively too large to do in an afternoon. Agentic sweeps collapse that category. Work that was uneconomic became routine, which is the single biggest thing that makes unbundled operations survivable for a small team.

The second-order effect is the one worth internalising. Once agents are deploying to staging and production, your contributor docs and agent instructions stop being courtesy files and become the context an executing system reads. Documentation moved from the lowest-leverage activity on the board to something close to the highest, because it is now load-bearing at runtime.

#How to tell which rung you belong on

The decision is about workload shape, not size. Answer honestly.

Question If yes If no
Is a request's peak memory predictable and small? Platform is fine You need a machine, or a queue
Does a request finish in under a second of compute? Platform is fine Move the work off the HTTP path first
Is your data model native to the store you're using? Keep going Fix this before migrating anything
Does first-request latency reach an evaluator? Price cold starts as acquisition Cold starts are tolerable
Do you have someone who will own patching and backups? A VPS is viable Stay managed and pay

That last row is the one people skip. A VPS is not free, it is unbundled. You take on OS patching, backups, certificate renewal, disk monitoring and intrusion basics. Those costs are real and recurring, and they simply do not appear on an invoice, which makes them easy to leave out of the comparison and expensive to discover.

#When to skip all of this

This is not an argument against managed platforms. It is an argument against using one for a workload it was not built for. Stay where you are if:

  • Your workload is I/O-bound and stateless. That is the shape free tiers are genuinely good at, and you will not beat them on price or effort.
  • You are still looking for product-market fit. Managed hosting is a service that buys back your attention, and attention is the scarcest thing you have pre-fit.
  • Nobody on the team wants to own a machine. An unpatched VPS is a worse outcome than a slow API, and it stays worse silently.
  • The bottleneck is a single hot path. Moving that one path to a background worker is far cheaper than migrating everything around it.

That last point deserves the concession, because it is where Rippli is heading anyway. The next milestone severs the heavy NLP work from the HTTP router and moves it behind a queue, so response time stops depending on query depth. Done early enough, that one change alone would probably have held the 512MB ceiling for considerably longer and saved us a rung of the ladder. It would not have touched the graph model. That was wrong on every platform we tried, and it would have stayed wrong on that one too.

#Key takeaways

  • Diagnose by workload shape, not size. Memory-bound, stateful, multi-process work does not belong on a free tier at any scale.
  • Treat OOM as its own failure class. It terminates rather than degrades, so there is no gradient to follow and you will misread it as flakiness.
  • Fix the data model before you change the platform. A bottleneck that follows you across three environments was never about the environment.
  • Colocate the datastore with the service that traverses it. Per-hop latency multiplies through a graph walk in a way it never does through a REST call, and it caps how deep your users are allowed to think.
  • Take on the deploy path the same week you take on operations. Improvised automation is where credentials leak.
  • Price a VPS as unbundled, not free. Patching, backups and monitoring are real recurring costs that never show up on the invoice.

Drawn from building Rippli, a financial intelligence platform that maps how companies are connected and traces how a shock in one propagates to the rest, through a migration from a browser IDE to a free cloud tier to self-hosted infrastructure, and from running its news ingestion engine in production alongside it. See the ripple. Act with clarity.

Shipping something like this?

Everything above came out of building and running the thing, not researching it. If you are hitting the same problems, I work with product and engineering teams on exactly this, architecture reviews, technical strategy, and staying on through implementation.