A viral traffic spike, spending alerts and a move to a VPS. Illustration; the numbers on screen are decorative.
A viral traffic spike, spending alerts and a move to a VPS. Illustration; the numbers on screen are decorative.

In-depth practical guide · Networks, servers and cloud · Updated: November 2, 2026

In short. My app for browsing the declared assets of public officials got roughly 100,000 unique visits and 600,000 page views in about 24 hours. I was away from my computer. About 12 hours into the spike, two notifications arrived: spending on Vercel had reached $50, and spending on Neon another $50. I reacted and moved the app to a cheaper VPS. This guide explains what happens technically at a moment like that, what Vercel and Neon actually charge according to their official pricing, how to cut down the number of expensive operations, how to set up financial protection that really stops something, and how to migrate to a VPS without losing data.

Introduction: when a traffic surge stops being only good news

Everyone who builds apps wants the moment when a project takes off: the link starts spreading, people write about it, and the visit counter climbs faster than ever. That moment is the reason we build and publish things in the first place.

There is, however, a side of viral success that gets talked about much less. Infrastructure that automatically absorbs more load also automatically raises the bill. The more elastic the platform, the faster it follows traffic, and spending follows with it.

I recently experienced this on my own project.

One of my apps, a tool for browsing and searching the assets of public officials, suddenly spread across social networks. In roughly 24 hours it attracted about 100,000 unique visits and about 600,000 page views. The app ran on Vercel and stored its data in a PostgreSQL database on Neon.

The problem was not that the app failed. It held up. The problem was that, because of private obligations, I was not around and was not watching what was going on.

About 12 hours later, notifications arrived that I could not ignore:

  • spending on Vercel had reached $50,
  • spending on Neon had reached another $50.

So roughly $100 of recorded spending on two services within a few hours, without a single planned cost. I did not want to wait and watch how much higher it would go. I checked the situation, estimated where the money was going, and moved the app to a cheaper VPS where the cost is predictable.

Before we start, a few important caveats, because this text is not meant to be a story about the "bad cloud":

  1. The time windows are not the same. The 100,000 visits refer to a period of roughly 24 hours, while the spending notifications arrived after about 12 hours. These are not two sides of the same equation.
  2. A notification is not an invoice. The $50 amounts are recorded spending from notifications, not necessarily the final invoice amount after all credits and billing.
  3. A notification does not show the cause. A message saying $50 has been spent does not tell you how much went to CPU, memory, data transfer or the database. That requires detailed usage data, which is what chapter one covers.
  4. Vercel and Neon are not bad platforms. Quite the opposite. Both are excellent for development and production. The problem arises when the pricing model does not fit the way the app is actually used.

10,000 visitors reading static pages and 10,000 visitors each triggering dozens of database queries are not the same thing. In the first case the cost can be negligible. In the second, a relatively small audience creates serious load.

Below we go through the whole problem: what happens during a sudden traffic spike, what the platforms really charge according to their price lists, how to find the most expensive part of an app, how to reduce cost with caching, how to stop bots, how to set up financial protection, what the math looks like for 10,000, 100,000 and one million visits, and how to migrate to a VPS, with concrete configurations for Next.js, PostgreSQL, Docker and Caddy.

The goal is not for everyone to leave the cloud. The goal is that nobody should have to wait for an unpleasant notification before seriously thinking about the cost of their own app for the first time.

What actually happens when an app goes viral

1. Visits, page views and HTTP requests are not the same thing

Before any analysis, separate the metrics that often get mixed up:

MetricWhat it meansWhy it matters for the bill
Unique visitorA browser or device that analytics recognizes as one visitorTells you how many people came, not how much work was done
Visit (session)One arrival at the site with a series of actionsOne person can have several sessions
Page viewLoading or displaying a page according to analytics rulesWith client-side navigation it does not have to mean a new HTML request
HTTP requestA single exchange with the CDN or serverOne page view can trigger dozens of requests for JS, CSS, images and APIs
Function invocationExecution of serverless or backend codeNot every request causes one; a cached response does not run a function
Database queryAn SQL operation on the databaseOne API call can trigger zero, one or ten queries

Platforms do not charge for "visits". They charge for requests, data transfer, processor time, memory and database work. So the key question is how much work each visit creates.

2. What 100,000 visits and 600,000 page views mean

The ratio of 600,000 page views to 100,000 visits gives roughly six page views per visit. That does not mean everyone opened exactly six pages. Some opened one, some opened twenty.

Let us build an illustration. Assume each page view triggers two API calls:

text
600,000 page views × 2 API calls = 1,200,000 API requests

If each of those calls runs two SQL queries, we theoretically get 2,400,000 queries against the database.

But with a good cache, a large share of those requests never reaches the database. If a call is unnecessary, it can be removed. If thousands of people ask for the same public data, it can be computed once and delivered to everyone.

That is the core of the whole story: you cannot control the number of people, but you can control how much work each visit creates, and by a lot.

3. Why a sudden spike is a different problem from gradual growth

A daily average often hides the real load. Six hundred thousand page views spread evenly over 24 hours gives about 6.94 page views per second, which modern infrastructure handles easily.

Viral traffic, however, is not even. A post from a popular account, sharing in large groups or a news segment sends a wave of visitors within minutes. That is why infrastructure is not judged by the total daily number, but by:

  • average and peak requests per second,
  • the number of concurrent users and open connections,
  • the duration of backend operations, especially p95 and p99 latency,
  • CPU and memory usage,
  • database load and the number of connections to it,
  • cache hit rate, especially for HTML and APIs,
  • errors and timeouts,
  • outbound traffic,
  • the cost incurred during the peaks themselves.

A particularly dangerous situation is one where the system works "well enough". Users do not notice a problem, pages open, data arrives. In the background, though, the system consumes more and more compute, transfer and database capacity. An elastic platform at that moment is doing exactly what it should: scaling. And billing.

4. How to determine what caused the cost

When a notification arrives, the first job is to react. But for a serious analysis afterwards, knowing that $50 was spent here and $50 there is not enough. You need to reconstruct the system's behavior from at least five sources:

SourceWhat we look for
Web analyticsVisits, page views, traffic sources, hourly distribution
Vercel UsageWhich billing items grew, by project and service
Vercel ObservabilityMost frequent routes and functions, duration, transfer, errors
Neon Billing and MonitoringCompute (CU-hours), compute size over time, storage, transfer
Application and access logsThe most expensive endpoints, repeated requests, bots, errors

Align the data by time, ideally in UTC, so the comparison is consistent. If the cost jumped at 2 p.m. and at the same time the number of calls to a particular API route jumped, that is a good lead. But correlation is not proof. Growth in function invocations can coincide with growth in transfer, database work or image optimization. Only the billing category, together with code and logs, shows where the money actually went.

Important: from the notification alone that $50 was spent, it is impossible to reliably determine how much went to CPU, memory, network or the database. That is why this text does not claim an exact technical cause for my bill, but shows how to find it.

What Vercel and Neon really charge

Spending alerts arriving after a viral spike. Illustration; the percentages and amounts on screen are decorative.
Spending alerts arriving after a viral spike. Illustration; the percentages and amounts on screen are decorative.

1. Vercel: one app, several independent billing items

Vercel is not just a server your app sits on. It is a global content delivery network, a function runtime, a cache, image optimization, a firewall and observability, and each of those layers has its own billing unit.

The Pro plan costs $20 per month and includes $20 of usage credit. Everything above that is billed by usage with no upper limit, unless you set one yourself. According to Vercel's pricing page, all new teams get a default on-demand budget of $200, with notifications, while automatic pausing at 100% is an option you have to enable separately.

Prices are regional. The region closest to Serbia is Frankfurt (fra1), so the table lists the official on-demand rates for Frankfurt, along with the range across all regions:

ItemFrankfurt (fra1)Range across regions
CDN Requests$2.60 per 1M requests$2.00 to $3.20
Fast Data Transfer$0.15 per GB$0.15 to $0.35
Fast Origin Transfer$0.06 per GB$0.06 to $0.43
Fluid Active CPU$0.184 per hour$0.128 to $0.221
Fluid Provisioned Memory$0.0152 per GB-hour$0.0106 to $0.0183
ISR Reads / Writes$0.52 / $5.20 per 1M units$0.40 to $0.64 / $4.00 to $6.40
Runtime Cache Reads / Writes$0.52 / $5.20 per 1M units$0.40 to $0.64 / $4.00 to $6.40
Image Optimization Transformations$0.0601 per 1K$0.05 to $0.0812
WAF Rate Limiting$0.65 per 1M allowed requests$0.50 to $0.80

Function invocations on the Pro plan cost $0.60 per million, and the Hobby plan includes one million invocations per month. Prices exclude tax and were checked on November 2, 2026 on the official regional pricing and Fluid compute pages.

What each item means in practice:

CDN Requests. Every request that reaches Vercel's network is a CDN request, and that includes cached responses. Caching saves more expensive function work, but it does not erase the request itself. One page view with 20 resources means 20 CDN requests.

Fast Data Transfer. Transfer from Vercel to the browser. Large images, heavy JavaScript bundles, PDFs and API responses push it up quickly. Vercel compresses responses on its own and adds ETag headers so browsers do not download the same files again.

Fast Origin Transfer. Transfer between the CDN layer and the runtime. It grows when a large share of requests bypasses the cache.

Fluid compute: CPU, memory and invocations. This is the most important difference from older serverless models:

  • Active CPU is billed only while code is actually running. While a function waits for a database or an external API, CPU is not billed.
  • Provisioned Memory is billed for the whole lifetime of an instance, including while the function waits on I/O. One instance, however, handles several requests at the same time, so memory is not proportional to the number of requests.
  • Invocations are counted for every request to a function, including those that end in an error.

Vercel's official example illustrates this well: a function that spends 100 ms processing data and 400 ms waiting for a database pays active CPU for only 100 ms. Memory, however, is paid for as long as the instance runs. That is why speeding up SQL queries does not reduce the CPU item, but it shortens the instance's lifetime and lowers memory.

Important for Next.js projects on Vercel: Vercel is merging the Data Cache into the Runtime Cache. According to the official announcement, Data Cache reads and writes that were previously unbilled will move to Runtime Cache pricing, and existing teams receive the migration details in advance. If your app relies heavily on fetch caching or unstable_cache, check how many reads and writes it makes before this item starts showing up on the bill.

There is also an alternative for projects with unpredictable traffic: Flat Rate CDN on the Pro plan. Instead of billing by CDN requests and transfer, you pay a fixed monthly price for the CDN, and temporary spikes do not raise the bill for those items. Functions and the database are still billed separately, so this solves only part of the problem, but it is worth considering before moving.

2. Neon: PostgreSQL billed by running time

Neon is PostgreSQL, but not in the classic "I rent a server that runs 24 hours a day" model. Compute and storage are separated. Compute scales with load and can shut down when the database is idle.

The basic unit is the CU (Compute Unit). One CU is roughly 4 GB of RAM, along with matching CPU and local SSD. The calculation is simple:

text
CU-hours = average compute size (CU) × hours running
Compute cost = CU-hours × price per CU-hour

Official Launch plan prices, checked on November 2, 2026:

ItemLaunch plan
Compute$0.106 per CU-hour
Storage$0.35 per GB-month
Instant restore history$0.20 per GB-month, based on the amount of change in the chosen window
Public network transfer500 GB per project per month included, then $0.10 per GB
Autoscalingup to 16 CU (64 GB RAM)
Scale to zeroafter 5 minutes of inactivity, can be disabled

A few calculations to get a feel for the scale:

text
1 CU × 10 h × $0.106 = $1.06
4 CU × 12 h × $0.106 = $5.09
1 CU × 730 h (full month) × $0.106 = $77.38
8 CU × 12 h × $0.106 = $10.18

The compute item alone for one intense day is not dramatic. But pay attention to two things that can multiply it: how long compute stays active and what size autoscaling pushes it to. A database that scales up to 8 or 16 CU under load and stays there for hours has a completely different bill from one running at 0.25 CU.

Autoscaling works within a range you set, for example from 2 to 8 CU. Too high a ceiling allows more expensive scaling. Too low a ceiling causes slowdowns and errors under load. The limit is both a technical and a financial decision.

Scale to zero shuts compute down after 5 minutes of inactivity and wakes it within a few hundred milliseconds on the next query. That is great for development and apps with occasional traffic. But persistent connections, periodic jobs, a health check that hits the database or logical replication keep the database from actually shutting down. Having scale to zero enabled does not automatically mean a minimal bill; check the real behavior.

Public network transfer is an item many people overlook. When an app on Vercel reads data from Neon, that data travels over the public internet and counts toward Neon's outbound transfer. On the Launch plan 500 GB per project is included, so this rarely becomes a problem, but it is worth watching for apps that return large datasets.

3. Connections and connection pooling

With serverless apps, pay special attention to how connections to PostgreSQL are opened. If hundreds of short-lived functions open direct connections at the same time, the database spends resources managing connections instead of running useful queries.

Neon offers pooling through a built-in PgBouncer, with up to 10,000 client connections. A pooled connection is obtained by adding the -pooler suffix to the endpoint name:

text
postgresql://user:password@ep-example-123456-pooler.eu-central-1.aws.neon.tech/dbname?sslmode=require

Two things to know:

  • 10,000 does not mean 10,000 concurrent queries. It is the number of client connections PgBouncer accepts; they share a much smaller number of real connections to PostgreSQL.
  • Pooling runs in transaction mode. Session-level features, SET statements meant to persist across a session, and schema migrations require a direct connection without the -pooler suffix.

4. Why my $50 on Neon does not prove a single cause

It is easy to conclude that the big traffic "spent $50 on SQL queries". Without detailed statistics, that conclusion is not precise enough. To reconstruct the bill you would need to know the compute size over time, how long the database was active, which categories were billed, whether other active resources existed (branches, history, replicas) and what spending looked like before the spike itself.

The numbers from my case are a real reason for analysis, but not proof of a single technical cause. That is the difference between a serious technical text and a story about how some platform is "too expensive".

Vercel, Cloudflare, VPS or hybrid: which architecture makes sense

1. There is no cheapest hosting for every app

Comparing platforms by a single monthly price is not enough. With a managed platform you pay for automation, global delivery, easy deployment, preview environments, administration and scaling. With a VPS you get raw resources and freedom, but you take on security, updates, backups, monitoring and recovery yourself.

A $15 or $20 per month server does not automatically become better infrastructure. For an app that needs high availability, redundancy and constant maintenance, the price difference is quickly eaten up by labor. For a relatively simple public app dominated by reads, the same VPS can be perfectly adequate.

2. Five architectures and their trade-offs

A) Vercel + Neon

text
User → Vercel CDN → Next.js function → Neon PostgreSQL

Fast delivery, simple changes, a small administrative burden. The problem arises when almost every visit triggers dynamic execution, which then triggers database queries. A well-optimized app on this pair can be very economical, especially when public pages are static and the database is used only when necessary. I would still recommend this combination for a large number of projects.

B) Cloudflare + Vercel + Neon

text
User → Cloudflare → Vercel → Neon

Cloudflare takes over part of the protection and delivery. But note: by default Cloudflare does not cache HTML or JSON, it caches by file extension and respects Cache-Control headers from the origin. Dynamic content has to be configured separately through Cache Rules. Two CDN layers require a clear understanding of cache headers, invalidation and forwarding the client IP address. Pages for signed-in users must never be cached indiscriminately.

C) A VPS with the app and the database

text
User → Reverse proxy → Next.js → PostgreSQL (same server)

Direct control over CPU, memory, disk and the database, with a predictable monthly price. For an app with many public reads and moderate writes, very economical. But a VPS has a ceiling: it does not turn itself into ten servers, and a single server is a single point of failure without a standby instance and a recovery plan.

D) Cloudflare + VPS + PostgreSQL

text
User → Cloudflare CDN/WAF → VPS reverse proxy → app → PostgreSQL

For a public app where cost control is the priority, this is one of the most interesting models. Cloudflare delivers static assets and filters part of the unwanted traffic, while the VPS runs the app and local queries. PostgreSQL must not be reachable from the internet, and the origin should be protected against direct requests that bypass Cloudflare.

E) Separate application and database servers

text
User → CDN/WAF → application VPS → private network → database VPS

Independent sizing and better isolation, at the cost of more configuration and more serious monitoring. For a small project it is not necessary just because it looks more professional.

3. Side-by-side overview

CriterionVercel + NeonCloudflare + VPSTwo VPS servers
Initial implementationSimpleModerately demandingMore demanding
Ongoing operationsLittleMoreMost
ElasticityStrongLimited by VPS resourcesRequires planning
Base price predictabilityDepends on usageHighHigh
Database administrationManagedSelf-managedSelf-managed
Backup and recoveryPartly managedYour responsibilityYour responsibility
Infrastructure controlMore limitedHighHigh

The table is not a quality ranking. It shows different trade-offs.

4. When I would choose a VPS today

If an app has a lot of publicly available data, predominantly read traffic, relatively predictable demands and monitoring and backups already prepared, a VPS is a serious option. An app for browsing public officials' assets is exactly that by nature: people search and read public data, and there are few writes.

If an app needs sudden horizontal scaling, high geographic availability, complex distribution or there is nobody to maintain the server, the advantages of managed infrastructure easily outweigh the price difference.

My move to a VPS was a response to a specific cost and a specific app, not a universal recommendation that everyone should leave Vercel or Neon.

Optimization: how to prevent millions of unnecessary operations

Caching, static generation and smarter API calls reduce origin load. Illustration; the percentages on screen are decorative.
Caching, static generation and smarter API calls reduce origin load. Illustration; the percentages on screen are decorative.

The biggest savings often come not from changing providers, but from changing the way the app handles requests. Move an inefficient app from one platform to another and you get the same inefficiency on a different server. That is why optimization goes layer by layer.

1. First layer: browser cache

Files whose name changes along with their content, such as hashed JavaScript and CSS, can be cached for a long time, and the browser does not request them at all on the next visit. Aggressive caching for files that change without a URL change, however, serves stale versions to users. Distinguish immutable resources from data that gets updated.

2. Second layer: CDN cache

A CDN serves a response from the nearest node, without going to the origin. It is useful to track response statuses, which differ by provider but usually look like this:

  • HIT: the response was found in cache,
  • MISS: it was not in cache and went to the origin,
  • BYPASS: the rules say to skip the cache,
  • EXPIRED / REVALIDATED: the content expired or was checked against the origin.

Do not look only at the overall hit rate. Track it separately for HTML, static assets and API responses. A high hit rate for small icons means nothing if every main API call still runs several expensive queries.

Cloudflare also has a useful mechanism for viral situations: cache lock. When many requests for the same uncached resource arrive at one data center at the same time, only the first goes to the origin, and the rest wait and receive the same response. That prevents a wave of visitors from hitting the origin for the same file at once.

3. Third layer: Next.js caching and rendering

For each kind of content, choose the rendering approach deliberately:

  • Static content (articles, documentation, rarely changing data) should not be generated for every visitor.
  • Dynamic content tied to a session, account or authorization needs a clear freshness and privacy policy.
  • ISR and cached components deliver content from cache and refresh it as needed, without constant re-rendering.

In Next.js 16 this is done through the Cache Components model. New apps created with create-next-app already have it, and in an existing app it is enabled in the configuration:

ts
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;

The documentation asks for partialPrefetching to be set explicitly, because Next.js logs a warning without it.

For public data that is safe to cache, create a cached function with a clear lifetime and a tag:

ts
// app/lib/public-statistics.ts
import { cacheLife, cacheTag } from 'next/cache';
import { db } from '@/lib/db';

export async function getPublicStatistics() {
  'use cache';

  cacheLife({
    stale: 300, // the client uses the cache for 5 minutes without checking
    revalidate: 900, // after 15 minutes the next request refreshes in the background
    expire: 3600, // after an hour without requests, fresh content is awaited
  });

  cacheTag('public-statistics');

  return db.statistics.getPublicSummary();
}

db.statistics.getPublicSummary() is an example of a function you write yourself, not a built-in method. The Next.js documentation recommends setting cacheLife in every use cache scope, so behavior does not depend on the default profile. In addition, expire must be longer than revalidate, otherwise Next.js reports an error.

When the source data changes, a trusted server-side part of the system invalidates the tag:

ts
import { revalidateTag } from 'next/cache';

// After a confirmed change to public data
revalidateTag('public-statistics', 'max');

The max profile uses stale-while-revalidate: the next request gets the existing content while new content is prepared in the background, so the user never waits. It is not a guarantee that every subsequent reader immediately sees the latest data. The single-argument form, revalidateTag(tag), is deprecated in Next.js 16 and should be replaced. For admin actions where the author must see their change immediately, Server Actions have updateTag.

Never expose a call like this through a public API route without authentication. Anyone who can call it can also keep emptying your cache.

Note for self-hosting: the cache in these examples works automatically for a single next start instance with a persistent disk. If you run several instances, or containers without a persistent disk, the Next.js documentation requires separate coordination of cache and invalidation, for example through a cacheHandler backed by Redis. Without it, each instance has its own cache and its own invalidation.

4. Fourth layer: duplicate API calls

One of the most common and most expensive problems in React apps is needlessly repeated requests. The causes are usually the same:

  • polling that runs too often,
  • several components independently requesting the same data,
  • no request deduplication,
  • wrong dependencies in useEffect,
  • automatic retries of failed calls without limits,
  • aggressive refetching on window focus,
  • calls that keep running when the tab is in the background.

The math is brutal. A page that refreshes data every three seconds for one user makes about 100 requests in five minutes. A thousand users with such a page open make about 100,000 requests in five minutes, even if the data may not have changed at all.

It is better to use a longer interval, conditional refreshing, HTTP revalidation through ETag headers, or events. WebSocket and Server-Sent Events are not automatically cheaper: they also hold connections and memory. Choose them based on how the app actually behaves.

5. Fifth layer: PostgreSQL optimization

When the database becomes the bottleneck, start with the real queries. What matters most is not which query is slowest, but which one consumes the most total time. A 300 ms query executed a million times is more expensive than a two-second query that runs ten times a day.

Track the number of calls per query, average and maximum duration, rows scanned and returned, index efficiency, open connections, locks and table sizes.

Check the execution plan like this:

sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, name, position, updated_at
FROM public_profiles
WHERE status = 'published'
ORDER BY updated_at DESC
LIMIT 30;

The table and columns are examples. Important: EXPLAIN ANALYZE actually executes the query. Running a heavy analysis against production under load can make things worse, so do it on a test copy or under controlled conditions.

If the analysis shows that a search pattern lacks a suitable index:

sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_profiles_status_updated
ON public_profiles (status, updated_at DESC);

Add an index only after analysis. Every index takes space and slows down writes, and even CONCURRENTLY uses resources and can take a long time.

Aggregations. Dashboards and home pages that run COUNT, SUM, GROUP BY or complex JOIN operations on large tables every time they open are especially dangerous. For data that does not need to be accurate to the second there are precomputed aggregates, materialized views with periodic refresh, an application cache, or caching of finished public API responses. Each of these needs a clear refresh policy.

6. Why this optimization is financially decisive

Take the 1,200,000 API requests from the earlier example:

text
10% cache:  1,200,000 × 0.90 = 1,080,000 requests reach the origin
90% cache:  1,200,000 × 0.10 =   120,000 requests reach the origin

Nine times fewer requests to functions and the database. That is not necessarily a bill nine times smaller, because CDN requests and transfer remain, but in terms of app and database load the difference is huge.

Bots, WAF and protection against financially expensive traffic

Rate limiting, WAF rules and bot filtering during viral growth. Illustration; the numbers on screen are decorative.
Rate limiting, WAF rules and bot filtering during viral growth. Illustration; the numbers on screen are decorative.

1. A lot of traffic is not automatically legitimate traffic

When an app becomes popular, it is not only people who come. Search engine crawlers, AI crawlers, link preview services for social networks and messengers, archiving tools, scrapers and malicious bots arrive too. They do not all carry the same risk. A crawler that indexes content is useful. A bot that constantly calls an expensive API endpoint can run up a bill even without a classic attack.

For an app with public data, such as a browser of officials' assets, this is especially important: data like that naturally attracts scrapers.

2. Rate limiting

Rate limiting restricts how many times the same source may hit a route within a given time. Vercel WAF Rate Limiting is available on all plans, with a fixed window that defaults to 60 seconds and 100 requests. Two official notes matter:

  • counters are kept per region, so traffic from several regions can together exceed a limit configured for one region,
  • requests allowed through a rate limit rule are billed, $0.65 per million in Frankfurt.

Limiting by IP address is not perfect. Many legitimate users share one public address behind a mobile carrier, a company or CGNAT, so an aggressive rule blocks real people. A large botnet, on the other hand, uses thousands of addresses. That is why signals are combined: path, method, headers, user identity when available, and per-account quotas.

With Cloudflare, the set of fields used for counting, the period lengths and the available actions depend on the plan, from Free to Enterprise with advanced rate limiting. Cloudflare itself warns that rate limiting verified bots can affect SEO.

3. WAF and platform protection

Vercel has two layers: a platform-wide firewall with automatic DDoS mitigation, free for everyone with no configuration, and a WAF for your own rules. Cloudflare offers a WAF, managed rules and bot protection. Introduce rules gradually while watching legitimate traffic, so users are not locked out.

4. Protecting the most expensive routes

Not all routes deserve the same protection. Make an overview by cost and risk:

Route typeRiskRecommended measure
Static contentHigh data transferCDN, compression, browser cache
Public list APIHuge number of callsCache, pagination, rate limit
SearchExpensive queriesLimits, indexes, input validation
Reports and aggregationsCPU and databasePrecomputed results, frequency control
Sign-inAutomated attemptsRate limit, security checks
Data exportLarge responsesQuotas, asynchronous processing, authorization
AdministrationUnauthorized accessStrict authentication and permissions

For public APIs, always limit response size, pagination depth, allowed filters and maximum execution time. If an endpoint accepts a limit parameter, the app must not let a client request an unlimited number of rows.

5. Why robots.txt is not protection

robots.txt is a signal for crawlers that follow the rules. It is not authentication and does not stop a bot that chooses to ignore it. An expensive or sensitive route needs real access control, caching, quotas and protection at the appropriate layer.

6. Protection must not become a new problem

Excessive protection blocks legitimate users, breaks sign-in, buries people in CAPTCHA checks, interrupts indexing of public content and can bring costs of its own. Measure how much traffic is filtered, which routes are affected, and whether requests to the origin actually go down. A measure is judged by its result, not by the number of blocked requests.

Financial monitoring: how not to be surprised by the bill

I consider this chapter the most important, precisely because of my own experience. While I was dealing with private obligations, the system kept running, and the notifications only later showed me how fast spending had grown. Financial monitoring must be as important as technical monitoring.

1. Vercel Spend Management: what it really does

Spend Management is available on the Pro plan and on the Enterprise plan with a Flexible Commitment contract, at no extra charge. It is managed by the Owner and Billing roles. You set an On-Demand Budget for the billing period, and when it is reached three actions can be triggered: notifications, a webhook call, and pausing production deployments of all projects in the team.

What is key, and what many people do not know:

  1. A budget by itself does not stop spending. To actually stop costs, the Pause Production Deployments option must be enabled separately.
  2. The check is not instant. Vercel checks spending every few minutes, so notifications, the webhook and pausing can trigger a few minutes after the threshold is crossed. The official recommendation is to set the budget below the amount you are truly willing to spend.
  3. The budget does not cover everything. On the Pro plan it covers usage above the monthly credit, for all projects in the team. It does not cover seats, Marketplace integrations or separate add-ons. If you connected Neon as a Vercel Marketplace integration, its spending is not part of this budget.
  4. Pausing means the site is unavailable. A paused production deployment does not serve traffic: the site, API and functions return a 503 DEPLOYMENT_PAUSED error. Each project has to be resumed manually, from the dashboard or through the REST API, and it will not resume on its own even when you raise the budget.
  5. Pausing does not stop all costs. It does not stop AI Gateway and v0 usage, and storage (deployment storage, Blob, registry) keeps being billed.

Web and email notifications arrive at 50%, 75% and 100% of the budget, and SMS can be enabled for the 100% threshold. Every member with the Owner or Billing role configures their own notifications, so check that they are really enabled on channels you watch even when you are away from your computer.

2. The budget from the command line

Since version vercel@59.23.0, the Vercel CLI has a vercel budgets command that manages the same budget as the dashboard. That means the budget can be managed by scripts and AI agents too, not just by people.

Overview of current spending and the budget:

bash
vercel budgets

A $200 budget that pauses production and calls a webhook when reached:

bash
vercel budgets set --amount 200 --pause --webhook https://example.com/spend

The CLI explicitly warns that all projects will be paused at 100% and production deployments will become unavailable. Alert history, since version vercel@65.2.0:

bash
vercel budgets alerts

Removing the budget without confirmation, useful in automation:

bash
vercel budgets remove --yes

The budget applies to the whole team, not to a single project. If the same team holds a critical business app and an experiment, the pause option shuts down both. In such cases it is better to split projects into different teams.

3. Neon: notifications, not a brake

On the Launch and Scale plans, Neon has Spending notifications. An organization admin sets a monthly dollar threshold in the Billing section of the console, and Neon emails the admins at 80% and 100% of the threshold. Spending is checked every 15 minutes, and reminders arrive weekly until you raise the threshold or a new billing period starts.

The most important sentence in the official documentation: notifications only send alerts, they do not stop usage. When the threshold is reached, projects keep running and charges keep accumulating. Automatic suspension is announced as a feature that is still coming.

If you need a real brake, Neon offers per-project consumption quotas through the Management API. They work per project and per metric (compute, storage, data transfer), not on a total dollar amount, and they can suspend compute when a quota is reached. For a production app with public traffic, this is the protection I would set up first.

4. The biggest oversight: watching each service separately

In my case, two services reached $50 each at the same time. If you look only at Vercel, everything seems under control. If you look only at Neon, the same. But infrastructure costs the sum of all services: hosting, database, storage, email, AI APIs, search, monitoring, protection. You need a single view of total spending.

5. An operating model with alert levels

For a more serious project, I would define levels based on the real budget:

LevelWhat it meansWhat to do
InformationalSpending has noticeably grownLook at the trend, no intervention
CheckGrowth is not explained by normal trafficFind the services, routes and requests that are growing
CriticalCost is growing faster than plannedLimit expensive functions, enable protection, stop jobs
Last lineContinuing costs more than stoppingAutomatically stop the expensive part or the whole service

The decision must not depend only on the amount. It must also take into account the damage from unavailability. For a hobby project, pausing is a reasonable last line. For a critical service, an outage can cost more than continued spending.

6. What to do when you are not around

This is what I felt most strongly. If an app can run up a serious bill while you are away for 12 hours, a notification you only see when you sit down at your computer is not protection.

For projects with financial risk, plan for:

  • mobile alerts on a channel you actually watch (SMS on Vercel at 100%, push from monitoring),
  • independent monitoring that does not depend on the same infrastructure whose problem it is supposed to detect,
  • predefined actions: what gets shut down, what gets limited, who decides,
  • a secure webhook receiver if you automate the response: verifying the authenticity of calls, protection against unauthorized triggering and a record of every action taken,
  • a short procedure: who responds, what they check first, which features they may temporarily turn off and how they confirm the problem is solved.

A practical model: 10,000, 100,000 and one million visits

1. Why nobody can say how much "100,000 visits cost"

One site handles 100,000 visits for a few dollars because almost everything is delivered statically. Another spends many times more for the same traffic because every user triggers complex queries, report generation, image processing or AI calls. That is why we build a transparent model with known assumptions. These are not results of a production test, but mathematical scenarios.

2. Assumptions

  • two API calls per page view,
  • 90% of API calls are served from cache,
  • 0.7 MB actually transferred per page view,
  • three page views per visit in the smaller scenario, six in the larger ones.

3. Three scenarios

Metric10,000 visits100,000 visits1,000,000 visits
Page views30,000600,0006,000,000
API requests60,0001,200,00012,000,000
Requests to origin with 90% cache6,000120,0001,200,000
Actual transfer21 GB420 GB4,200 GB

Individual requests for images, CSS, fonts and JavaScript are not included here, because their repetition depends on browser and CDN caching. CPU, memory and the database are not included either.

4. What 420 GB can mean on Vercel

If, purely as an illustration, we applied the Frankfurt rate of $0.15 per GB:

text
420 GB × $0.15 = $63

At the most expensive regional rate of $0.35 it would be $147. But this is not a bill estimate. The Pro credit, billing period, region, Flat Rate CDN and other categories can completely change the amount. A bill is not calculated by multiplying total traffic by a public unit price.

5. The formula for Neon

text
Compute = Σ (CU × active hours × $0.106)

If compute size changes during the day, each interval is calculated separately. An example with a constant 2 CU for 100 hours and 5 GB of storage for the whole month:

text
2 × 100 × $0.106 = $21.20  (compute)
5 × $0.35        =  $1.75  (storage)
Total            = $22.95  before other categories

6. Why a peak is not the same as total daily traffic

Suppose a system handles 10 requests per second on average, and during a viral peak it gets 300. By Little's law, in a stable system the number of operations in progress at the same time is roughly the arrival rate times the average duration:

text
300 requests/s × 0.2 s = about 60 concurrent operations

If under congestion the duration stretches to two seconds, the same arrival rate gives about 600 operations in progress or waiting. That is why p95 and p99 latency matter more than the average, and a database slowdown can cause an avalanche: longer processing keeps more instances active, and more active instances mean more billed memory.

7. Interactive calculator

Below is a model that calculates the same quantities for your own assumptions, using the official rates for Vercel Frankfurt and Neon Launch. Try the ready-made scenarios, then change the values.

Interactive model · tutorijali.app

Calculator: how much work and money a viral day creates

Change the assumptions and watch how visits turn into requests, function invocations, data transfer and database work. Prices are public on-demand rates for Vercel Frankfurt (fra1) and Neon Launch, checked on November 2, 2026.

01 / trafficWho arrives and how much they open
In my case about 100,000 in roughly 24 hours.
600,000 views / 100,000 visits gives about 6.
HTML, JS, CSS, images, fonts. A cached request is still a CDN request.
Bytes actually transferred after compression and browser cache.
02 / backendWhat reaches the functions
Calls that can start a function and hit the database.
Share of calls served from cache, without a function.
Only the time the code is really running, not waiting on the database.
Includes waiting on the database and external APIs.
Check your project settings. The model ignores instance sharing.
03 / database and serverNeon and the alternative
1 CU is about 4 GB of RAM, plus CPU and local SSD.
The database only bills while it runs; scale to zero after 5 minutes.
Average database size over the month.
Your offer, including backup and transfer. Labor is not included.
600,000page views
12,000,000CDN requests
1,200,000API calls before cache
480,000calls reaching functions
420 GBestimated transfer

Vercel, gross at fra1 rates

CDN requests12,000,000 × $2.60 / 1M$31
Fast Data Transfer420 GB × $0.15$63
Function invocations480,000 × $0.60 / 1M$0.29
Active CPU$0.184 per hour$0.98
Provisioned Memory$0.0152 per GB-h, upper bound$1.42
Total before credit$97

Neon Launch

Compute24 CU-h × $0.106$2.54
Storage5 GB × $0.35 per month$1.75
Total$4.29

Public network transfer from Neon is included up to 500 GB per project per month, then $0.10 per GB. Restore history, branches and other services are not modeled.

Comparison in the same unit

Vercel + Neon for the entered period$101
VPS for a full month$25

Note: the left bar is the cost for the entered traffic period only, the right bar is a full month of server time. A VPS has a capacity ceiling and does not scale by itself; compare it with your real peak, not only with the bill.

How to read the results:

  1. First change the cache hit rate and watch how much the number of calls reaching functions drops.
  2. Then reduce the transfer per page view. At millions of page views, even tens of kilobytes per page make a difference.
  3. Finally change the size and hours of Neon compute. You will see that running time and compute size matter more than the number of queries.

The model deliberately does not show a falsely precise final bill. A visit count and a few averages are not enough for that.

Moving to a VPS: the complete technical procedure

Moving from a managed environment to a VPS with a reverse proxy and monitoring. Illustration; the prices and percentages on screen are not project data.
Moving from a managed environment to a VPS with a reverse proxy and monitoring. Illustration; the prices and percentages on screen are not project data.

1. First, determine whether the move pays off

Migration is not automatically the right answer to the first larger bill. Compare at least three options:

  1. optimization on the existing platform (caching, static pages, Flat Rate CDN, autoscaling limits),
  2. partial migration, for example only the database or only the backend,
  3. full migration to a VPS.

When comparing, count the total cost of ownership: the server, extra disks, backups and snapshots, transfer above the included limit, monitoring, protection, the domain and, most importantly, the time for administration and recovery. A VPS that is cheaper on the invoice but takes several hours of your work each week may not be cheaper. For many small and medium public apps, though, a self-maintained VPS remains an excellent price-to-resources ratio.

2. Inventory of the existing infrastructure

Before migrating, write down:

  • the runtime and exact versions of Node.js and Next.js,
  • the exact major version of PostgreSQL and all required extensions,
  • all environment variables,
  • background jobs and cron tasks,
  • upload and storage locations,
  • webhook URLs, DNS records, authentication settings,
  • email and other external integrations,
  • existing backup and restore procedures,
  • platform-specific features (Vercel cron, Image Optimization, Edge Config, analytics) that need replacing on a VPS.

Pay special attention to data that is not in the database itself. The database may reference files stored elsewhere; transferring SQL data will not move those files.

3. Choosing capacity

A server is not chosen by visit count but by measurements: peak CPU load, app and database memory, the number of concurrent connections, disk I/O demands, database and working set size, network traffic and headroom for backups and deployments. A VPS with a few vCPU cores and several gigabytes of RAM is a good starting point for a simple public app, but it is not a guarantee it will handle 100,000 visits. Capacity is confirmed by testing. Choose a region with good latency for your users.

4. Basic server protection

On a fresh Ubuntu Server installation, first update the system:

bash
sudo apt update
sudo apt upgrade

Then set up an admin account, SSH key access and a firewall. With UFW, before enabling it, make sure SSH is allowed:

bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

On a remote server a wrong rule can lock you out completely, so keep your existing session open and make sure the provider has a fallback console. Do not open PostgreSQL port 5432 to the internet. If the app and the database run on the same server, exposing the database publicly is not necessary.

5. Next.js for self-hosting: standalone

Next.js officially supports running on your own Node.js server, and for Docker the most practical option is the standalone output:

ts
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'standalone',
};

export default nextConfig;

The build creates .next/standalone with a minimal server.js. Two things from the documentation that are easy to miss: server.js does not copy the public folder or .next/static (they are meant for a CDN), so you copy them manually, and the port and address are set with the PORT and HOSTNAME variables. Next.js also recommends placing a reverse proxy in front of the server to handle malformed requests, slow connections, request size limits and rate limiting.

6. Production Docker image

An example for npm and Node.js 24 LTS. Check compatibility with your project before using it:

dockerfile
FROM node:24-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:24-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:24-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER node
EXPOSE 3000
CMD ["node", "server.js"]

The example assumes the project has a public folder and a package-lock.json. Add a .dockerignore so local node_modules, secrets and unnecessary files do not end up in the build. In a more serious environment, pin base image versions and scan them.

7. Docker Compose: PostgreSQL, the app and Caddy

yaml
services:
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  app:
    build: .
    restart: unless-stopped
    environment:
      NODE_ENV: production
      DATABASE_URL: ${DATABASE_URL:?required}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - backend
      - frontend

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app
    networks:
      - frontend

networks:
  frontend:
  backend:

volumes:
  postgres_data:
  caddy_data:
  caddy_config:

Several decisions in this configuration are deliberate:

  • PostgreSQL has no ports section, so it is not published on a public port of the server.
  • Two networks: Caddy is only on the frontend network and has no access to the database, while the app is on both.
  • The database version (postgres:17) should match the major version you use on Neon, so pg_restore runs without surprises.
  • Secrets are in an .env file that is not in the repository. If the password in DATABASE_URL contains special characters, it must be URL-encoded.

8. Caddy and automatic HTTPS

Caddy automatically obtains and renews HTTPS certificates when the domain points to the server and ports 80 and 443 are reachable:

caddyfile
example.com {
    encode zstd gzip
    reverse_proxy app:3000
}

For an existing production domain, it is advisable to test the new infrastructure on a separate subdomain first. Caddy simplifies TLS, but it does not replace the rest of the protection: headers, request size limits, monitoring and correct handling of forwarded IP addresses, especially if Cloudflare is in front.

9. Migrating the database from Neon

This is the most sensitive part of the process. Before starting, check version compatibility, extensions, encoding and collation, roles and permissions, sequences, foreign keys, procedures and triggers, as well as data size.

Export. pg_dump makes a consistent snapshot of the database even while others are using it. For exporting from Neon use a direct connection, not a pooled one, and client tools of the same or a newer major version:

bash
pg_dump "$NEON_DIRECT_URL" -F c -O -x -f database.dump

The options are: -F c for the custom format, -O to skip object ownership and -x to skip privileges. The file contains all the data, so treat it as sensitive.

Restore into a prepared empty database on the VPS:

bash
pg_restore -d "$TARGET_DATABASE_URL" -O -x -e database.dump

-e stops the restore at the first error instead of continuing and leaving a half-restored database. Success is not confirmed by a message saying the command finished. Compare row counts in key tables, sequence values, critical data and real user flows.

10. The key problem: writes during migration

pg_dump captures the state at one moment. Anything written after that does not exist in the target database. That is why "dump, restore, change DNS" is not a safe plan for an app that accepts writes. There are two strategies.

Strategy A: a short, controlled write freeze. For smaller apps, the simplest and safest:

  1. prepare and test the new infrastructure,
  2. do a trial migration and measure how long it takes,
  3. announce a short maintenance window if needed,
  4. stop new writes on the old app,
  5. wait for active transactions and jobs to finish,
  6. make the final dump,
  7. restore it into the target database,
  8. check integrity and critical features,
  9. switch writes to the new database,
  10. return to normal operation only after verification.

A mostly read-only app, such as a browser of public data, handles this strategy easily, because reading can continue while writes are temporarily paused.

Strategy B: minimal downtime. Larger systems use logical replication or dedicated tools: an initial copy, continuous change synchronization, lag monitoring, a coordinated final cutover and consistency checks. It is more complex, with limitations around DDL changes, sequences, extensions and permissions. That is why it is not responsible to promise a migration "without a single second of downtime" based on a generic script. A short, pre-planned write freeze is often the safer choice.

11. Switching traffic and a rollback plan

When DNS changes, different resolvers and browsers remember the old answer for a while. Lowering the TTL in advance helps, but it does not guarantee an instant switch. The most important rule: during the transition, the old and new app must not accept independent writes into two different databases, because diverging data is hard to fix.

Rollback is not just restoring the old DNS address. If the new app has already accepted writes, going back to the old database without those changes means losing data. The plan must say from what moment new writes are accepted, how they are carried back, what the criteria are for aborting the migration and who makes the decision.

12. Backups after migration

By moving from a managed database to your own PostgreSQL, you also take on responsibility for the data. A Docker volume is not a backup, and a disk snapshot is not a substitute for a verified database backup. Distinguish:

  • backup: a copy you can recover from, stored off the server as well,
  • snapshot: an image of the disk or virtual machine, useful but not sufficient,
  • point-in-time recovery: restoring to an exact moment, with WAL archiving.

Define the RPO (how much data you can afford to lose, expressed in time) and the RTO (how long the system may be unavailable during recovery). A backup you have never restored is not a backup, it is hope.

13. A CDN in front of the VPS

If you put Cloudflare in front of the VPS, define clearly which routes may be cached, which always bypass the cache, how public content is invalidated, how sessions are kept, whether the origin accepts direct requests, how the real IP address is logged and what the rate limiting rules are. A wrongly cached private page is not a performance problem, it is a security problem.

Testing, monitoring and a production checklist

1. Types of tests

  • A smoke test checks that basic features work under minimal load.
  • A load test checks expected and increased load.
  • A stress test looks for the system's limit and how it fails.
  • A spike test simulates a sudden surge, exactly what going viral brings.
  • A soak test uncovers problems that appear only after long operation, such as memory leaks and piling up connections.

2. A k6 test example

javascript
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 10 },
    { duration: '1m', target: 10 },
    { duration: '30s', target: 0 },
  ],
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<1000'],
  },
};

export default function () {
  const res = http.get('https://staging.example.com/health');
  check(res, { 'HTTP 200': (r) => r.status === 200 });
  sleep(1);
}

This is a small demonstration test, not a simulation of 100,000 users. Success on /health does not prove the other features will hold up. A realistic test models the most common user flows, the real mix of reads and writes and database behavior. Run it only against systems you are authorized to test, and keep in mind that the test itself costs money on a managed platform.

3. What to monitor after moving to a VPS

LayerMetrics
ApplicationRequests per second, p50/p95/p99, 4xx and 5xx errors, Node.js process memory, event loop stalls
SystemCPU and load, free RAM, swap, disk IOPS and latency, free space, network transfer, container restarts
PostgreSQLActive connections, most expensive queries, lock waits, database size, cache hit rate, checkpoints and WAL, backup success
SecurityBlocked requests, unusual access patterns, sign-in attempts, firewall changes, security updates
FinancesServer price, extra transfer, backup and storage, external APIs, time spent on administration

4. Production checklist

Before exposing the app to a large audience again:

Application

  • Critical user features have been tested.
  • There are no duplicate or unnecessary API calls.
  • Public data is cached where it is safe.
  • Private data cannot end up in a public cache.
  • The API has limits on size, pagination and time.
  • The most expensive queries have been identified.
  • There are no infinite retry loops.

Server

  • SSH is protected with keys.
  • The firewall allows only necessary ports.
  • The database is not reachable from the internet.
  • The system and containers are updated regularly.
  • TLS works and renews automatically.
  • Logs are rotated with limited retention.

Database

  • Version and extensions have been verified.
  • Connections cannot grow without limit.
  • Backups are automated and stored off the server.
  • Restoring data has actually been tested.
  • RPO and RTO are defined.

Costs and monitoring

  • Both technical and financial alerts exist.
  • All platforms that bill for resources are known.
  • Critical notifications arrive even when you are away from your computer.
  • On Vercel it is clear whether pausing is enabled and what it means.
  • On Neon there are per-project quotas, not just notifications.
  • There is a procedure for sudden traffic growth and for excessive spending.

Viral growth

  • Normal traffic and at least one measured peak are known.
  • The limits of the current infrastructure are known.
  • The most expensive routes have dedicated protection.
  • Legitimate traffic is not needlessly blocked.
  • There is a plan for degraded operation and a person on call.

FAQ: frequently asked questions

Is 100,000 visits a lot?

It depends on the app. For a static site it is a light load. For an app that generates dynamic reports or hits the database heavily, a serious challenge. What decides it is the number and cost of real operations and how they are distributed over time.

Is Vercel too expensive for viral projects?

There is no universal answer. A well-optimized app on Vercel can be very economical, and Flat Rate CDN removes the risk of CDN items spiking. Some usage patterns, however, create significant costs. Look at the billing metrics and compare them with the alternative.

Is Neon a bad choice for an app with a lot of traffic?

No. Neon has properties that are excellent for developing and scaling PostgreSQL. You need to understand CU-hours, autoscaling limits, scale to zero and connection behavior, and set up per-project quotas. A managed database can be a good choice even when the app runs on a VPS.

Can a $10 or $20 VPS handle 100,000 visits?

It can, but it is not guaranteed. For well-cached public content it is doable. For a demanding app with many writes and complex queries it is probably nowhere near enough. Only measurement gives a responsible answer.

Is Cloudflare enough to remove all the cost?

No. It can significantly reduce certain kinds of load, but it does not remove the need to run the app and the database, and by default it does not cache HTML or JSON. Some of its features also have their own prices.

Is a budget alert a hard limit?

Not necessarily. On Vercel, pausing is enabled separately and triggers with a delay of a few minutes. Neon's notifications only send an email and do not stop spending; per-project quotas through the API are what actually stop it.

Does automatic pausing always make sense?

No. For a hobby project it is reasonable for the app to stop temporarily. For a critical business app an outage can be much more expensive than continued spending. The rules are adjusted to business risk.

Is a Docker volume a backup?

No. A volume keeps data beyond the container's lifecycle, but it is not an independent, verified copy. When the disk fails or the data gets corrupted, the volume does not help.

Does a DNS change guarantee migration without downtime?

No. DNS caching, active connections and writes during the transition create complexity that is not visible from the IP change itself. With the database, the most important thing is to ensure data consistency.

Does a cheaper VPS mean cheaper infrastructure?

Not automatically. You need to count backups, monitoring, maintenance, security, recovery and your own time. For many projects, though, the price difference does justify the extra administration.

Conclusion: the most expensive thing is not hosting, but the lack of control

In about 24 hours, the app for browsing public officials' assets attracted roughly 100,000 unique visitors and 600,000 page views. While I was away because of private obligations, spending kept growing. After about 12 hours the notifications arrived: $50 on Vercel and another $50 on Neon. I reacted and moved to a cheaper VPS, better suited to this particular app.

The most important conclusion, however, is not that a VPS is better than Vercel or Neon. This matters more:

  1. The number of visits does not tell you how demanding an app is. The number of requests, function invocations, queries and transferred bytes does.
  2. Caching, an optimized backend and good SQL queries are often worth more than changing providers.
  3. Filtering unnecessary traffic is not only a security question but a financial one.
  4. Costs are monitored as a whole. An alert for one service does not cover the others.
  5. A budget and a notification are not a brake. On Vercel pausing must be enabled, and on Neon real protection comes from per-project quotas.
  6. Plan for the moments when you are not around. An app does not stop receiving users because you are asleep, traveling or busy.
  7. Migration makes sense only with preparation: data protection, tested backups, monitoring and a realistic capacity estimate.

Today it is easier than ever to build an app and publish it. AI agents and modern tools have shortened the road from idea to production to a few days. That is exactly why it is more important than ever to understand the infrastructure that will carry the app when people actually discover it.

A successful app is not one that works with ten users, but one that stays usable, secure and financially sustainable under unexpected load. Going viral is a great thing. Only good preparation keeps that success from turning into an unplanned expense.

Methodology and sources

Prices and service behavior were checked on November 2, 2026 in the official documentation. Prices change, so check the current pages before making a decision. Financial examples that are not from my own experience are mathematical illustrations, not invoices or production test results. Commands and configurations are examples that should be adapted to your own app and tested in a staging environment before production.

The images in this article are AI-generated conceptual illustrations. They do not show my real Vercel or Neon account, and the numbers on their screens, such as cache percentages, server prices or savings percentages, serve only as a visual representation of the topic.

Vercel

Neon

Next.js

Cloudflare, PostgreSQL, Caddy and k6