Serverless functions vs VMs is not a question of old infrastructure versus new infrastructure. It is a question of operating model.
A serverless function is best when a small piece of code should run because something happened: an HTTP request arrived, a cron schedule fired, a file landed in object storage, or a one-off task needs to run. A VM is best when your team needs a full server environment: a long-lived app process, custom packages, persistent services, system-level control, or predictable always-on capacity.
For Raff Technologies users, the practical answer is usually not "functions or VMs forever." The better answer is: use Raff Functions for event-driven work and use Raff VM when the workload needs a full box. Many production systems use both.
At Raff, we see the same pattern often: teams start by putting every task on one VM because it is simple. That works until background jobs, cron tasks, webhooks, image processing, API callbacks, and cleanup scripts start competing with the main app. Serverless functions help when those jobs should scale or run independently. VMs remain the right home for the main application when it needs a stable runtime and full operating-system control.
This guide explains when a small team should use serverless functions, when a VM is still the cleaner choice, and how to combine both without making the architecture harder than the product needs.
Serverless functions and VMs solve different operations problems
A VM gives you a complete server environment.
You choose the operating system, install packages, run services, manage processes, configure networking, store files, run containers, and keep the environment alive. This is useful when the workload behaves like an application server, database host, worker machine, or self-managed runtime.
A serverless function gives you a smaller deployment unit.
You ship code and configuration. The platform handles the runtime, scaling behavior, trigger wiring, and execution environment. This is useful when the workload behaves like a task: handle this request, process this file, run this scheduled job, send this webhook, resize this image, or transform this payload.
The difference is not only technical. It changes how the team thinks.
| Question | Serverless function | VM |
|---|
| What runs? | A function or small service | A full server environment |
| When does it run? | On trigger or demand | Continuously or manually controlled |
| Who manages the OS? | Platform | Your team |
| How does it scale? | Per function or trigger | By VM sizing or additional VMs |
| What is the cost shape? | Usage-based | Capacity-based |
| What is best hosted there? | HTTP handlers, cron, webhooks, event tasks | Apps, databases, containers, persistent workers |
| What can go wrong? | Timeout, cold start, trigger limits, stateless design issues | Patch burden, idle cost, noisy services, manual scaling |
| Best buyer question | "Can this task run only when needed?" | "Does this workload need a whole environment?" |
Neither model is universally better. The right choice depends on what the workload is, how often it runs, how much control it needs, and what the team can operate safely.
The quick decision framework
Use this first.

A useful rule:
Use a function when the unit of work is clear. Use a VM when the environment is the product.
If the application needs a stable server, a VM is usually cleaner. If the work is event-driven and isolated, a function may reduce operations work.
Use serverless functions when work is event-driven
Serverless functions are strongest when work starts because something happened.
Good function workloads include:
- Webhook receivers
- Image resizing
- PDF generation
- File conversion
- Scheduled cleanup
- Email sending
- Object storage event processing
- API callbacks
- Lightweight HTTP APIs
- Data enrichment
- One-off future tasks
- Batch transforms
- ETL steps
- Notification fanout
- Form submission handling
- Report generation
The pattern is simple:

This is cleaner than keeping a VM awake only to run a small task every few minutes.
Raff Functions supports HTTP live URLs with automatic TLS, timezone-aware cron schedules, one-off future runs, and object-storage events such as bucket uploads with prefix filters. That makes functions a natural fit for tasks that do not need a full server process.
Use VMs when the workload needs a full environment
A VM is still the better choice when the workload needs a complete server.
Use a VM when you need:
- A long-running web app
- A Docker Compose environment
- Custom system packages
- Full SSH access
- A specific Linux distribution
- Persistent local services
- Manual process control
- Long-lived TCP connections
- Custom network configuration
- Sidecar services
- Self-hosted databases or queues
- Full filesystem control
- Background workers that run constantly
- Predictable always-on capacity
- Existing deployment scripts built around servers
A VM is especially useful when the team wants to keep architecture simple.
For example, an early SaaS app may run perfectly on one Raff VM:
Raff VM
↓
App runtime or Docker Compose
↓
Nginx reverse proxy
↓
Systemd services
↓
Database or external managed database
That setup is often easier to understand, debug, and operate than splitting every task into separate functions too early.
A function is not a replacement for every server. It is a better fit for certain kinds of work.
The cost model is different
VM cost is mostly capacity-based.
You choose a server size and pay for the capacity while it exists. If your app is busy all day, that can be efficient. If the VM sits idle most of the time, you still pay for the reserved capacity.
Function cost is mostly usage-based.
You pay when code runs, usually based on runtime resources. This can be efficient for spiky, occasional, or event-driven work. It can become less attractive when the workload is constantly running or when it needs a large runtime environment for long periods.
Raff Functions bills on two meters: memory and active CPU. Requests and egress are free, and a spend cap is on by default. Raff also provides a monthly free tier of 100,000 GB-seconds and 10,000 vCPU-seconds.
This makes Raff Functions useful for teams that want cost to follow execution instead of server uptime.
But cost comparison should always match the workload.
| Workload shape | Cost risk on VM | Cost risk on function |
|---|
| Rare scheduled job | Paying for idle capacity | Usually low |
| Spiky webhook traffic | Overprovisioning or manual scaling | Usage spikes need spend control |
| Always-on app server | Efficient if fully used | May be less natural |
| Heavy batch job | VM may need temporary scale-up | Duration and memory must be planned |
| File processing after uploads | VM can become noisy | Good fit if runtime is bounded |
| Constant worker queue | Predictable VM may be simpler | High continuous usage may add up |
A function is not automatically cheaper. It is cheaper when usage-based execution matches the workload.
A VM is not automatically wasteful. It is efficient when the workload uses the capacity consistently.
Runtime and portability matter
Serverless platforms can create lock-in when the handler format, event structure, deployment model, or runtime assumptions are proprietary.
For small teams, portability matters because the codebase is still changing. The team may move between local development, VMs, containers, and functions as the architecture matures.
Raff Functions is designed around standard handlers:
- Python FastAPI / ASGI
- Node.js standard
http
- TypeScript
- JavaScript Web Fetch
- Go
net/http
- Any language through a Dockerfile escape hatch
That matters because the code should not need a major rewrite just to move in or out of a functions environment.
A good portability rule:
The closer a function looks like normal application code, the easier it is to maintain.
This is especially useful when a team begins on a VM, extracts one task into a function, and later decides whether that task should remain a function, become a worker, or move into Kubernetes.
Cron jobs are a natural functions use case
Cron jobs often start small and become messy.
One script runs every night. Then another runs every hour. Then a cleanup job runs every five minutes. Then a billing job, report job, import job, and notification job appear. Eventually, the main app VM becomes responsible for too many background tasks.
Functions are a clean fit for many scheduled jobs because they run only when needed.
Good cron-function candidates include:
- Deleting temporary files
- Sending daily reports
- Syncing external APIs
- Checking expired subscriptions
- Rotating short-lived tokens
- Creating invoices
- Running light data cleanup
- Updating cached summaries
- Checking failed webhook retries
- Processing small recurring imports
A VM-based cron job is still fine when the script needs local system access, shares a complex app environment, or runs constantly enough that a server process is simpler.
The decision is not "cron is old, functions are new." The decision is whether the scheduled work deserves a full server environment.
For a deeper background-work comparison, connect this article with Cron Jobs vs Queues vs Workflow Automation.
Webhooks are usually better isolated
Webhooks are one of the best reasons to use functions.
A webhook endpoint usually receives an external event, validates the request, writes a record, triggers work, and returns quickly. It does not usually need the whole application server environment.
A function can isolate webhook handling from the main app:
External service
↓
Raff Function webhook endpoint
↓
Validate signature
↓
Write event to database or queue
↓
Return response
This is useful for:
- Stripe-style billing events
- GitHub events
- CRM callbacks
- Email delivery callbacks
- Form submissions
- Monitoring alerts
- Marketplace events
- Automation workflows
- Third-party API notifications
The benefit is operational isolation. A webhook spike does not need to hit the main application server directly. The function can validate, normalize, and pass work to the right place.
For production, webhooks still need clear rules:
- Validate signatures
- Return quickly
- Store event IDs
- Make processing idempotent
- Handle retries
- Avoid doing heavy work inside the request path
- Send long work to a queue or worker
- Log enough context for debugging
Functions make webhook routing easier, but correctness still depends on application design.
Databases and files should live outside functions
Functions should not become a hidden database.
A function should usually read or write durable data through a proper data layer:
Function
↓
Managed database for records
↓
Object storage for files
Use a database for:
- Users
- Accounts
- Orders
- Payments
- Audit logs
- App state
- Job status
- Metadata
- Durable business records
Use object storage for:
- Uploads
- Images
- Exports
- Reports
- Generated files
- Backups
- Media
- Attachments
- Artifacts
Raff Functions can bind to Raff Managed Databases and Raff Object Storage with scoped credentials. This keeps functions stateless while data lives in the right service.
This matters because serverless code should be easy to scale, retry, and replace. If files or critical state live inside the execution environment, the architecture becomes fragile.
For related reading, use App Uploads: VM Disk vs Object Storage for SaaS Teams and Database Backup Strategy for SaaS Apps.
Long-running functions change the batch decision
Traditional serverless decisions often break around duration.
A small handler is easy. A long AI inference, ETL process, file conversion, export, import, or streaming task is harder. Some teams move these tasks to a VM only because the functions platform has a short timeout.
Raff Functions supports timeouts up to 1 hour by default and up to 24 hours on request.
That does not mean every long job should be a function. It means the decision can be based on architecture instead of an artificial duration ceiling.
Use a long-running function when:
- The job is triggered by an event
- The job can run independently
- The environment is not too heavy
- Progress and failures are visible
- Output is written to database or object storage
- The job does not need a full VM
- The team wants usage-based execution
Use a VM when:
- The job runs constantly
- The environment is large or custom
- The task needs full OS control
- The worker depends on long-lived local state
- Multiple processes coordinate on the same machine
- Manual debugging through SSH is important
- Capacity-based cost is simpler
For example:
| Job | Function fit | VM fit |
|---|
| Resize uploaded images | Strong | Also possible |
| Generate a monthly PDF report | Strong | Possible |
| Run a constant queue worker | Depends | Strong |
| Train a long-running custom ML process | Depends | Stronger if environment is complex |
| Process rare CSV imports | Strong | Possible |
| Run a browser automation worker all day | Depends | Often VM |
| Nightly cleanup script | Strong | Possible |
Longer function timeouts are useful, but the team should still design around retries, failure visibility, and idempotency.
VMs remain better for persistent services
Some workloads want to stay alive.
A web server listens continuously. A database maintains state. A queue worker may run all day. A reverse proxy handles routing. A Docker Compose stack may contain several services that expect stable networking and local process control.
Those workloads are natural on a VM.
A VM is also better when the team wants to inspect the environment directly:
ssh into server
check logs
restart service
inspect process
change config
run migration
debug network
That direct control is not always elegant, but it is useful for small teams. Many early production apps benefit from a simple server that the developer understands deeply.
Serverless functions reduce server management for event-driven work. VMs reduce platform abstraction for full-environment workloads.
Both forms of simplicity are valid. The mistake is choosing one model for every workload.
The strongest small-team architecture often uses both
For many SaaS apps, the best architecture is mixed.
A simple production architecture might look like this:

This keeps the main app stable while moving event-driven work out of the server.
Examples:
| Use case | Main home | Why |
|---|
| Customer-facing app | VM | Stable runtime and full control |
| Database | Managed database | Durable state and managed operations |
| Uploads | Object storage | Files should not live in compute |
| Image resize | Function | Runs after upload event |
| Billing webhook | Function | Isolated external event handling |
| Daily report | Function | Scheduled task without server cron |
| Heavy constant worker | VM | Predictable always-on workload |
| API callback | Function | Event-driven and isolated |
| Admin dashboard | VM | Part of core app |
This approach avoids the two common extremes:
- Putting every background task on the app VM
- Splitting the app into functions before the product needs that complexity
A good architecture should make the product easier to operate next month, not just more modern today.
Raff-specific path from VM to functions
A cost-aware Raff path can be staged.
Stage 1: One Raff VM
Start with one VM when the app is early.
Raff VM
↓
App + background jobs + simple cron
This is clear and easy to operate.
Stage 2: Move durable data out
Move the database to Raff Managed Databases when data reliability and operations matter.
Raff VM
↓
Raff Managed Database
This separates compute from persistent records.
Stage 3: Move files out
Move uploads, reports, media, and generated files to Raff Object Storage.
App VM
↓
Managed Database for metadata
↓
Object Storage for files
This prevents file growth from forcing VM changes.
Move webhooks, cron, file processing, and one-off jobs into Raff Functions.
Raff Function
↓
Managed Database
↓
Object Storage
This reduces background-job pressure on the app VM.
Stage 5: Use more infrastructure only when needed
If workloads keep growing, add worker VMs or Kubernetes when coordination justifies it.
Raff VM + Raff Functions
↓
Worker VMs or Raff Kubernetes when needed
This path lets infrastructure grow with the product instead of forcing a big platform decision too early.
Common mistakes to avoid
Moving the whole app to functions too early
A full SaaS app is often easier to run on a VM at first.
Functions are excellent for event-driven pieces, but splitting every route, job, and process into functions can make debugging harder before the team has enough traffic to justify it.
Keeping every background task on the main VM
The opposite mistake is letting the main app server run everything.
If cron jobs, webhooks, file processing, reports, and cleanup scripts compete with the customer-facing app, functions can help isolate the work.
Forgetting that functions should be stateless
Functions should write durable records to a database and durable files to object storage.
Do not depend on local runtime state for business-critical data.
Ignoring timeouts and retries
Even with longer execution windows, functions need failure handling.
Design for retries, idempotency, duplicate events, and clear logs.
Comparing only monthly price
Compare operating model, not only sticker price.
A VM may be cheaper for constant usage. A function may be cheaper for spiky tasks. The real question is which model reduces both infrastructure cost and operations cost.
Creating too many tiny services
Every new deployment unit adds monitoring, naming, security, environment variables, ownership, and debugging work.
Extract functions when there is a clear operational reason.
Using functions to hide poor architecture
If the database is slow, uploads are stored in the wrong place, or jobs lack retry logic, functions will not automatically fix the design.
Fix the architecture underneath.
Serverless functions vs VMs comes down to workload shape
Serverless functions and VMs are both useful for small teams. They solve different problems.
Use serverless functions when work is event-driven, isolated, scheduled, bursty, or triggered by HTTP, cron, object storage, or one-off events. Use VMs when the app needs a full environment, long-lived processes, custom packages, persistent services, Docker Compose, SSH access, or predictable always-on capacity.
For Raff teams, the cleanest path is usually mixed: Raff VM for the main application, Raff Managed Databases for durable records, Raff Object Storage for files, and Raff Functions for event-driven work that should run independently.
Choose the model that makes the workload easier to operate for the next 6-12 months.