//JorgenHoc
← All articles
.NET HostingBy Jorge CalderónUpdated 17 min read

.NET on Render.com — Pros, Cons, and Real Benchmark

Host your .NET 10 app on Render.com: setup guide, render.yaml config, cold start problem, pricing breakdown, and a benchmark comparison against Railway and Fly.io.

#dotnet#cloud#devops

Render.com has quietly become one of the most developer-friendly PaaS platforms for containerized workloads — but its free tier cold-start penalty and pricing jumps trip up .NET developers who don't know what they're signing up for. This guide covers everything from Dockerfile to render.yaml, a real deployment of the .NET 10 sample in samples/dotnet-hosting, and an honest comparison with Railway and Fly.io.

What Render.com Is (and Where It Sits in the Market)

Render is a fully managed cloud platform that runs Docker containers, static sites, cron jobs, and background workers. It occupies the middle ground between Heroku (dead or expensive) and raw Kubernetes: you get Git-driven deploys, managed TLS, and built-in Postgres without writing infrastructure code.

Its main differentiators for .NET developers:

  • Zero-config HTTPS on every service and custom domain
  • render.yaml — infrastructure-as-code checked into your repo
  • Native Postgres managed databases with automatic backups
  • Private networking between services on the same account (no public egress charges)
  • Auto-deploy on push to any branch you configure

The catch: the free tier spins down idle services, and the jump from free ($0) to Starter ($7/month) is the smallest upgrade that eliminates cold starts.

Dockerfile for .NET on Render

Render runs any Docker image. The standard multi-stage build works without modification — this is the one the sample deploys with:

# Dockerfile — multi-stage: SDK image builds, smaller ASP.NET image runs
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["JorgenHoc.DotnetHosting.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
 
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "JorgenHoc.DotnetHosting.dll"]
⚠️

Render injects a PORT environment variable at runtime (10000 by default). The tempting Dockerfile line ENV ASPNETCORE_URLS=http://+:${PORT:-8080} does NOT pick it up — Docker resolves ${...} when the image is built, so the fallback gets baked in and Render's injected value is silently ignored. Read PORT in Program.cs instead.

// Program.cs — pick up Render's runtime-injected PORT
var port = Environment.GetEnvironmentVariable("PORT");
if (!string.IsNullOrEmpty(port))
{
    builder.WebHost.UseUrls($"http://+:{port}");
}
// Without PORT, the aspnet base image default (8080) applies.

Deploying from the Dashboard

The fastest path is the dashboard form — no YAML needed. New → Web Service, connect the GitHub repo, and one form covers everything. The only non-obvious field for a monorepo is Root Directory, which points the Docker build context at the right folder:

Render's New Web Service form for the jorgenhoc-org/dotnet-samples repository: name jorgenhoc-hosting-sample, Language set to Docker, branch main, region Oregon US West, and Root Directory samples/dotnet-hosting for the monorepo.
The entire Render deployment configuration in one form: repo, Docker, branch, and Root Directory for the monorepo.

Pick an instance type (Free for this walkthrough), click Deploy Web Service, and Render builds the Dockerfile and routes traffic once the health check passes:

Render service dashboard for jorgenhoc-hosting-sample showing the service Live on the Free instance, the onrender.com URL, deploy logs with ASP.NET Core request logging, and a banner warning that free instances spin down with inactivity, which can delay requests by 50 seconds or more.
Live on the Free tier — note the banner: Render itself warns that a spun-down free instance can delay requests by 50+ seconds. That's the cold start section below.

The deployed sample reports the platform it detected (via the RENDER environment variable) and the port Render injected:

Browser showing the JSON response from jorgenhoc-hosting-sample.onrender.com: service JorgenHoc hosting sample, runtime 10.0.11, platform Render, listeningOn http://[::]:10000.
The deployed sample's response: .NET 10, platform detected as Render, and listening on 10000 — the PORT Render injected at runtime, proof the Program.cs approach works.

render.yaml — Infrastructure as Code

The dashboard form works, but render.yaml (a "Blueprint") declares services, databases, and environment variables in a file checked into your repo — Render applies it when you connect the repo as a Blueprint. The sample keeps a minimal one at deploy/render.yaml; a fuller production-shaped example:

# render.yaml — checked into source control
services:
  - type: web
    name: my-dotnet-api
    runtime: docker
    region: oregon          # oregon | frankfurt | singapore | ohio
    plan: starter           # free | starter | standard | pro
    branch: main            # auto-deploy on push to this branch
    healthCheckPath: /health
 
    # Build-time overrides (rarely needed with a Dockerfile)
    dockerfilePath: ./MyApi/Dockerfile
 
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Production
      - key: ConnectionStrings__DefaultConnection
        fromDatabase:
          name: my-postgres-db    # references the database block below
          property: connectionString
      - key: JWT_SECRET
        generateValue: true       # Render generates a random value once
 
    autoDeploy: true
 
  # Background worker (no public port, no health check required)
  - type: worker
    name: my-background-worker
    runtime: docker
    plan: starter
    branch: main
    dockerfilePath: ./Worker/Dockerfile
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Production
 
databases:
  - name: my-postgres-db
    databaseName: myapp
    user: myapp_user
    plan: free              # free | basic-256 | basic-512 | basic-1
    region: oregon

Key points:

  • fromDatabase.property: connectionString injects the full Postgres connection string automatically — no copy-pasting credentials.
  • generateValue: true is useful for secrets like JWT signing keys; Render generates them once and never shows them again in logs.
  • The healthCheckPath tells Render's load balancer which endpoint to poll. Return HTTP 200 when the app is ready.

Health Check Endpoint

Add a minimal health endpoint so Render knows your app is up:

// Program.cs
app.MapGet("/health", () => Results.Ok(new { status = "healthy", utc = DateTime.UtcNow }));

For production, use Microsoft.Extensions.Diagnostics.HealthChecks to include database connectivity:

builder.Services.AddHealthChecks()
    .AddNpgsql(connectionString, name: "postgres");
 
// Map at /health
app.MapHealthChecks("/health");

Environment Variables

Render provides two ways to set environment variables:

1. render.yaml (source-controlled, non-secret values)

envVars:
  - key: ASPNETCORE_ENVIRONMENT
    value: Production
  - key: FEATURE_FLAG_NEW_CHECKOUT
    value: "true"

2. Dashboard (secrets, per-service overrides)

Navigate to your service → Environment → Add Environment Variable. Values set in the dashboard override render.yaml for the same key. Never put secrets in render.yaml — use the dashboard or generateValue: true instead.

Render also exposes built-in variables you can read in your app:

VariableValue
RENDERtrue
RENDER_SERVICE_NAMEYour service name
RENDER_GIT_COMMITFull SHA of deployed commit
RENDER_GIT_BRANCHBranch name
PORTPort your server must bind to
// Read Render-specific context for logging/tracing
var isRender = Environment.GetEnvironmentVariable("RENDER") == "true";
var commitSha = Environment.GetEnvironmentVariable("RENDER_GIT_COMMIT") ?? "local";
var serviceName = Environment.GetEnvironmentVariable("RENDER_SERVICE_NAME") ?? "unknown";
 
logger.LogInformation("Starting {Service} @ {Commit}", serviceName, commitSha[..7]);

Adding a Postgres Database

The databases block shown above creates a managed Postgres instance. Render links it to your service via fromDatabase, which automatically injects the ConnectionStrings__DefaultConnection environment variable.

// appsettings.json — key must match the envVar key in render.yaml
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Database=myapp;Username=dev;Password=dev"
  }
}
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

Via Dashboard

  1. Dashboard → New → PostgreSQL
  2. Choose region (match your web service region to avoid cross-region latency)
  3. Copy the Internal Database URL — this is the private network URL, free egress
  4. Paste it into your service's environment variables as ConnectionStrings__DefaultConnection
💡

Always use the Internal Database URL (starts with postgres://...@dpg-...oregon-a:5432/...) rather than the external URL. Internal connections stay within Render's private network — no egress costs and ~0.3ms lower latency vs the public endpoint.

Running EF Core Migrations on Deploy

Render does not have a built-in migration hook. Common approaches:

Option 1 — Migrate on startup (simple, suitable for small teams)

// Program.cs — run migrations before the app starts accepting requests
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    // ApplyMigrations is idempotent — safe to run on every startup
    await db.Database.MigrateAsync();
}

Option 2 — Pre-deploy job (safer for production)

Add a preDeployCommand in render.yaml (currently in beta for Docker services via shell scripts). Alternatively, use Render's one-off job feature to run migrations manually before promoting.

Custom Domains and TLS

  1. Dashboard → your service → Settings → Custom Domains → Add Custom Domain
  2. Add the CNAME record your DNS provider requires (Render shows the exact value)
  3. Render provisions a Let's Encrypt certificate automatically — usually within 60 seconds
  4. HTTP → HTTPS redirect is enabled by default

You can add multiple custom domains per service. Wildcard certificates (*.yourdomain.com) are not supported on the free tier.

Auto-Deploy from GitHub

render.yaml sets autoDeploy: true and branch: main. Every push to main triggers a new Docker build and rolling deploy.

To deploy preview environments per pull request, enable Pull Request Previews under your service's Settings. Each PR gets its own URL (https://my-dotnet-api-pr-42.onrender.com) with isolated environment variables.

For branch-based staging environments, create a second service in render.yaml:

services:
  - type: web
    name: my-dotnet-api-staging
    runtime: docker
    plan: starter
    branch: develop           # auto-deploy staging from develop branch
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Staging

The Free Tier Cold Start Problem

This is the most common complaint about Render and the thing that matters most before choosing a plan.

What happens: Free tier web services spin down after 15 minutes of inactivity. The next HTTP request wakes the container. During the wake-up, Render must:

  1. Pull the Docker image (cached, fast)
  2. Start the container
  3. Wait for your app to pass its health check

For a typical .NET API, this takes 25–60 seconds. ASP.NET startup is not fast — it scans assemblies, initializes DI, warms up EF Core, and connects to Postgres before accepting traffic.

The first request after a cold start either hangs for 30–60 seconds or returns a 502 (if the health check timeout is shorter than your startup time).

Measuring Your Cold Start

Add startup timing to understand where time is spent:

// Program.cs
var sw = System.Diagnostics.Stopwatch.StartNew();
 
var builder = WebApplication.CreateBuilder(args);
// ... service registration ...
 
var app = builder.Build();
// ... middleware pipeline ...
 
app.Lifetime.ApplicationStarted.Register(() =>
{
    sw.Stop();
    // Log to stdout — visible in Render's log viewer
    Console.WriteLine($"[STARTUP] Ready in {sw.ElapsedMilliseconds}ms");
});
 
await app.RunAsync();

Cold Start Workarounds

Option 1 — External health check ping (free)

Use a free uptime monitor (UptimeRobot, Better Uptime, Cronitor free tier) to ping your /health endpoint every 10 minutes. This keeps the container warm without upgrading.

Limitations: the monitor itself can have gaps, and pings don't count as user traffic — Render's inactivity timer is based on HTTP requests from external clients, not your own monitoring.

Option 2 — Upgrade to Starter ($7/month)

The Starter plan eliminates cold starts entirely. The service runs continuously, and you get 512 MB RAM and a shared CPU. For most .NET APIs, this is the right choice the moment you have real users.

Option 3 — Optimize startup time

Reduce cold start duration by deferring expensive initialization:

// Use IHostedService for background initialization instead of blocking startup
builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddHostedService<MyServiceWarmup>();
 
// MyServiceWarmup.cs
public class MyServiceWarmup : IHostedService
{
    private readonly IMyService _service;
    public MyServiceWarmup(IMyService service) => _service = service;
 
    public async Task StartAsync(CancellationToken ct)
    {
        // Run expensive init in background — doesn't block health check
        _ = Task.Run(() => _service.WarmUpAsync(ct), ct);
    }
 
    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
⚠️

If your app takes more than Render's health check timeout (default 180 seconds) to start, the deploy will fail and Render will roll back to the previous version. Increase the timeout under Service Settings → Health Check Timeout, or optimize startup.

Pricing

PlanPrice/monthRAMCPUCold StartsBest For
Free$0512 MBSharedYes (15 min idle)Demos, side projects
Starter$7512 MBSharedNoPersonal projects, low traffic
Standard$252 GB1 vCPUNoProduction APIs
Pro$854 GB2 vCPUNoHigh-traffic services
Pro Plus$1758 GB4 vCPUNoCPU-intensive workloads
Render's Instance Type picker with the Free tier selected at $0 per month for 512 MB RAM and 0.1 CPU, alongside paid tiers: Starter $7 for 512 MB and 0.5 CPU, Standard $25 for 2 GB and 1 CPU, Pro $85 for 4 GB and 2 CPU, up to Pro Ultra $450. A note warns that free instances spin down after periods of inactivity.
The instance picker during setup — the same prices as the table above, straight from the dashboard, with the spin-down caveat attached to Free.

Postgres pricing (separate from web service):

PlanPrice/monthStorageRAM
Free$0256 MB256 MB
Basic-256$7256 MB1 GB
Basic-512$14512 MB1 GB
Basic-1$281 GB2 GB
Pro$65100 GB4 GB

Free Postgres expires 30 days after creation (no backups) — after that Render deletes the database. For persistent free data, use an external provider like Neon; for production, use at least Basic-256.

Bandwidth: 100 GB free per month across all services; $0.10/GB after that.

Estimated monthly cost for a typical .NET API + Postgres:

  • Development: Free web + Free Postgres = $0 (cold starts, 30-day DB limit)
  • Production: Starter web + Basic-256 Postgres = $14/month
  • Scaled production: Standard web + Basic-1 Postgres = $53/month

Benchmark: Render vs Railway (Real Numbers)

These numbers were actually measured (August 2026) against the two live deployments from this article series — the sample .NET 10 API on Render Free (Oregon) and Railway Hobby (US East). Fly.io was planned as the third platform but is not measured: it no longer has a free tier and signup requires a credit card.

Test setup:

  • Tool: k6, script at benchmark/k6-latency.js — run it against your own deployments
  • Load: fixed arrival rate of 30 req/s for 60 seconds (~1,800 requests per platform), services warmed first
  • Endpoint: /, a small JSON payload, no database — this measures the platform's HTTP path, not query performance
  • Client: a single machine in Bogotá, Colombia. With a fixed arrival rate, the latency distribution is dominated by the network path between the client and the provider's region — these numbers tell you what a user in that location experiences, not some absolute platform speed. Rerun the script from where your users are.

Warm Latency

ProviderPlanRegionp50p95p99Failed requests
RenderFree ($0)Oregon152 ms206 ms318 ms0 of 1,800
RailwayHobby ($5)US East181 ms220 ms312 ms0 of 1,801

The surprise: Render came out faster at the median despite its origin being farther away (Oregon vs US East). The reason is the edge network — Render sits behind Cloudflare, which terminated TLS in Bogotá, while Railway's nearest edge was Miami. By p99 they are indistinguishable. The honest conclusion: at 30 req/s on the cheapest tiers, both platforms are comfortable, and your users' geography matters more than the platform's compute.

Cold Start (first request after 22 minutes idle)

Provider1st request2nd request3rd request
Render Free12.3 s0.63 s0.45 s
Railway Hobby0.69 s0.45 s0.47 s

Render Free had spun down and needed 12.3 seconds to boot the container, start .NET, and pass the health check. Note that this minimal API is a best case — no DI graph, no EF Core warm-up, no database connection. A real application lands closer to the 25–60 second range described earlier, which is why Render's own dashboard banner warns of delays of "50 seconds or more." Railway Hobby never sleeps, so its "cold" number is just a fresh TLS handshake.

Key takeaways:

  • Both cheapest tiers handled 30 req/s with zero failed requests and sub-330 ms p99 from South America.
  • Edge networks beat origin proximity at the median: Render + Cloudflare was faster from Bogotá than Railway's closer origin region.
  • The real free-tier difference is the cold start: Render Free pays 12+ seconds after every idle period; Railway has no free web tier, so nothing ever sleeps.
  • Fly.io is unmeasured here — if you have an account, deploy the same sample with its fly.toml and run the same script.

Pros and Cons

Pros

  • render.yaml is excellent — the cleanest infra-as-code of the three platforms. Define everything in one file, diff it in PRs.
  • Managed Postgres is first-class — automatic daily backups, point-in-time recovery on higher plans, seamless connection string injection.
  • PR preview environments — each PR gets a live URL with isolated config. Massive for QA workflows.
  • Zero networking config — internal service discovery just works. Workers can call web services by name with no extra setup.
  • Transparent pricing — no surprise egress bills. 100 GB/month free bandwidth covers most small apps.
  • DX is polished — deploy logs are real-time, rollbacks are one click, environment variable diffs are auditable.

Cons

  • Free tier cold starts are brutal for .NET — 30–60 second wake-ups are worse than most platforms because of .NET's startup overhead. Acceptable for demos, unusable for real users.
  • Build times depend on your Dockerfile — structure it for layer caching (restore before copy) or you'll feel every deploy.
  • No edge/multi-region — Render has 4 regions but no automatic geo-routing or CDN-level distribution. Fly.io wins here.
  • Shared CPU on Starter is limited — .NET apps with CPU-intensive tasks (PDF generation, image processing) will throttle on Starter and need Standard ($25).
  • No GPU support — not a platform for ML inference workloads.
  • Free Postgres expires after 30 days — easy to forget, painful when your database disappears in production.

When to Choose Render

ScenarioVerdict
Side project / demoFree tier is fine — accept cold starts
Internal tooling, low trafficStarter ($7) — best value
Production API, moderate trafficStandard ($25) — solid choice
Need closest-to-user latencyConsider Fly.io instead
No credit card / hard $0 budgetRender Free is the only real option of the three
Full infra-as-code in one fileRender wins with render.yaml
Managed Postgres with backupsRender is excellent
PR preview environmentsRender is the best of the three

Summary

Render.com is a strong choice for .NET developers who want a clean PaaS experience without managing infrastructure. The render.yaml format is the best infra-as-code story of any comparable platform, managed Postgres is genuinely good, and PR previews are a workflow multiplier.

The free tier is only suitable for demos and development — the cold start problem with .NET is too severe for anything user-facing. Budget $14/month (Starter + Basic Postgres) as the minimum for a production app. At that price point, Render is hard to beat for the combination of developer experience, reliability, and managed services.

If raw latency is your primary constraint, run the k6 script above from your users' region — our measurements show geography and edge networks dominate at this scale. But for most .NET teams shipping a web API or background worker, Render delivers everything you need in one place.

Further reading

About the author

Jorge Calderón

Software engineer with over a decade building and operating .NET applications in production — EF Core data layers, async-heavy services, and Azure and container deployments. Every benchmark and sample project in these guides is published in a public GitHub repository so you can rerun it yourself.

GitHub profileLinkedIn ↗Benchmarks & sample code

Related articles