//JorgenHoc
← Todos los artículos
.NET HostingPor Jorge CalderónActualizado 19 min read

.NET en Render.com — Pros, Contras y Benchmark Real

Aloja tu app .NET 10 en Render.com: guía de configuración, render.yaml, el problema de cold start, desglose de precios y comparativa de rendimiento con Railway y Fly.io.

#dotnet#cloud#devops

Render.com se ha convertido silenciosamente en una de las plataformas PaaS más amigables para desarrolladores que trabajan con cargas de trabajo en contenedores — pero la penalización de cold start del nivel gratuito y los saltos de precios sorprenden a los desarrolladores .NET que no saben en qué se están metiendo. Esta guía cubre todo, desde el Dockerfile hasta render.yaml, un despliegue real del ejemplo .NET 10 de samples/dotnet-hosting, y una comparación honesta con Railway y Fly.io.

Qué es Render.com (y su posicionamiento en el mercado)

Render es una plataforma cloud completamente gestionada que ejecuta contenedores Docker, sitios estáticos, cron jobs y workers en segundo plano. Ocupa el punto intermedio entre Heroku (obsoleto o muy caro) y Kubernetes puro: obtienes deploys basados en Git, TLS gestionado y Postgres integrado sin escribir código de infraestructura.

Sus principales diferenciadores para desarrolladores .NET:

  • HTTPS sin configuración en cada servicio y dominio personalizado
  • render.yaml — infraestructura como código versionada en tu repositorio
  • Postgres nativo con bases de datos gestionadas y backups automáticos
  • Red privada entre servicios de la misma cuenta (sin costos de egress público)
  • Auto-deploy en cada push a la rama que configures

El inconveniente: el nivel gratuito suspende los servicios inactivos, y el salto de gratuito ($0) a Starter ($7/mes) es la mejora más pequeña que elimina los cold starts.

Dockerfile para .NET en Render

Render ejecuta cualquier imagen Docker. El build multi-etapa estándar funciona sin modificaciones — este es el que usa el ejemplo en su despliegue:

# Dockerfile — multi-stage: la imagen SDK construye, la imagen ASP.NET (más pequeña) ejecuta
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 inyecta una variable de entorno PORT en tiempo de ejecución (10000 por defecto). La tentadora línea de Dockerfile ENV ASPNETCORE_URLS=http://+:${PORT:-8080} NO la recoge — Docker resuelve ${...} cuando la imagen se construye, así que el fallback queda fijado y el valor inyectado por Render se ignora silenciosamente. Lee PORT en Program.cs en su lugar.

// Program.cs — leer el PORT que Render inyecta en tiempo de ejecución
var port = Environment.GetEnvironmentVariable("PORT");
if (!string.IsNullOrEmpty(port))
{
    builder.WebHost.UseUrls($"http://+:{port}");
}
// Sin PORT, aplica el puerto por defecto de la imagen base aspnet (8080).

Despliegue desde el Dashboard

El camino más rápido es el formulario del dashboard — sin YAML. New → Web Service, conecta el repositorio de GitHub, y un solo formulario lo cubre todo. El único campo no obvio en un monorepo es Root Directory, que apunta el contexto de build de Docker a la carpeta correcta:

Formulario New Web Service de Render para el repositorio jorgenhoc-org/dotnet-samples: nombre jorgenhoc-hosting-sample, Language en Docker, rama main, región Oregon US West y Root Directory samples/dotnet-hosting para el monorepo.
Toda la configuración del despliegue en Render en un formulario: repo, Docker, rama y Root Directory para el monorepo.

Elige un tipo de instancia (Free para este recorrido), haz clic en Deploy Web Service, y Render construye el Dockerfile y enruta tráfico una vez que pasa el health check:

Dashboard del servicio jorgenhoc-hosting-sample en Render mostrando el servicio Live en la instancia Free, la URL onrender.com, logs de deploy con el logging de peticiones de ASP.NET Core y un banner que advierte que las instancias gratuitas se suspenden por inactividad, lo que puede retrasar las peticiones 50 segundos o más.
Live en el nivel Free — nota el banner: el propio Render advierte que una instancia gratuita suspendida puede retrasar peticiones 50+ segundos. Eso es la sección de cold start de abajo.

El ejemplo desplegado informa la plataforma que detectó (mediante la variable de entorno RENDER) y el puerto que Render inyectó:

Navegador mostrando la respuesta JSON de jorgenhoc-hosting-sample.onrender.com: service JorgenHoc hosting sample, runtime 10.0.11, platform Render, listeningOn http://[::]:10000.
La respuesta del ejemplo desplegado: .NET 10, plataforma detectada como Render y escuchando en 10000 — el PORT que Render inyectó en tiempo de ejecución, prueba de que el enfoque de Program.cs funciona.

render.yaml — Infraestructura como Código

El formulario del dashboard funciona, pero render.yaml (un "Blueprint") declara servicios, bases de datos y variables de entorno en un archivo versionado en tu repo — Render lo aplica cuando conectas el repositorio como Blueprint. El ejemplo mantiene uno mínimo en deploy/render.yaml; un ejemplo más completo con forma de producción:

# render.yaml — versionado en control de código fuente
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 en cada push a esta rama
    healthCheckPath: /health
 
    # Overrides en tiempo de build (raramente necesario con un Dockerfile)
    dockerfilePath: ./MyApi/Dockerfile
 
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Production
      - key: ConnectionStrings__DefaultConnection
        fromDatabase:
          name: my-postgres-db    # referencia al bloque de base de datos
          property: connectionString
      - key: JWT_SECRET
        generateValue: true       # Render genera un valor aleatorio una vez
 
    autoDeploy: true
 
  # Worker en segundo plano (sin puerto público, sin health check requerido)
  - 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

Puntos clave:

  • fromDatabase.property: connectionString inyecta automáticamente el connection string completo de Postgres — sin copiar y pegar credenciales.
  • generateValue: true es útil para secretos como las claves de firma JWT; Render los genera una vez y nunca los vuelve a mostrar en los logs.
  • El healthCheckPath indica al balanceador de carga de Render qué endpoint consultar. Devuelve HTTP 200 cuando la app está lista.

Endpoint de Health Check

Agrega un endpoint de health mínimo para que Render sepa que tu app está activa:

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

Para producción, usa Microsoft.Extensions.Diagnostics.HealthChecks para incluir la conectividad con la base de datos:

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

Variables de Entorno

Render provee dos formas de configurar variables de entorno:

1. render.yaml (versionado en código, valores no secretos)

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

2. Dashboard (secretos, overrides por servicio)

Navega a tu servicio → Environment → Add Environment Variable. Los valores configurados en el dashboard sobreescriben render.yaml para la misma clave. Nunca pongas secretos en render.yaml — usa el dashboard o generateValue: true.

Render también expone variables integradas que puedes leer en tu app:

VariableValor
RENDERtrue
RENDER_SERVICE_NAMENombre de tu servicio
RENDER_GIT_COMMITSHA completo del commit desplegado
RENDER_GIT_BRANCHNombre de la rama
PORTPuerto al que debe enlazarse tu servidor
// Leer contexto específico de Render para 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("Iniciando {Service} @ {Commit}", serviceName, commitSha[..7]);

Agregar una Base de Datos Postgres

Via render.yaml (recomendado)

El bloque databases mostrado arriba crea una instancia de Postgres gestionada. Render la vincula a tu servicio mediante fromDatabase, que inyecta automáticamente la variable de entorno ConnectionStrings__DefaultConnection.

// appsettings.json — la clave debe coincidir con la clave envVar en 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. Elige la región (hazla coincidir con la región de tu servicio web para evitar latencia entre regiones)
  3. Copia la Internal Database URL — esta es la URL de la red privada, sin costo de egress
  4. Pégala en las variables de entorno de tu servicio como ConnectionStrings__DefaultConnection
💡

Siempre usa la Internal Database URL (comienza con postgres://...@dpg-...oregon-a:5432/...) en lugar de la URL externa. Las conexiones internas permanecen dentro de la red privada de Render — sin costos de egress y ~0.3ms menos de latencia vs el endpoint público.

Ejecutar Migraciones de EF Core en el Deploy

Render no tiene un hook de migración integrado. Enfoques comunes:

Opción 1 — Migrar al inicio (simple, adecuado para equipos pequeños)

// Program.cs — ejecutar migraciones antes de que la app empiece a aceptar peticiones
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    // ApplyMigrations es idempotente — seguro ejecutarlo en cada inicio
    await db.Database.MigrateAsync();
}

Opción 2 — Job previo al deploy (más seguro para producción)

Agrega un preDeployCommand en render.yaml (actualmente en beta para servicios Docker). Como alternativa, usa la función de jobs puntuales de Render para ejecutar migraciones manualmente antes de promover.

Dominios Personalizados y TLS

  1. Dashboard → tu servicio → Settings → Custom Domains → Add Custom Domain
  2. Agrega el registro CNAME que requiere tu proveedor DNS (Render muestra el valor exacto)
  3. Render aprovisiona un certificado de Let's Encrypt automáticamente — generalmente en 60 segundos
  4. La redirección de HTTP a HTTPS está habilitada por defecto

Puedes agregar múltiples dominios personalizados por servicio. Los certificados wildcard (*.tudominio.com) no están soportados en el nivel gratuito.

Auto-Deploy desde GitHub

render.yaml establece autoDeploy: true y branch: main. Cada push a main dispara un nuevo build Docker y un deploy progresivo.

Para desplegar entornos de previsualización por pull request, habilita Pull Request Previews en la configuración de tu servicio. Cada PR obtiene su propia URL (https://my-dotnet-api-pr-42.onrender.com) con variables de entorno aisladas.

Para entornos de staging basados en ramas, crea un segundo servicio en render.yaml:

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

El Problema del Cold Start en el Nivel Gratuito

Esta es la queja más común sobre Render y lo que más importa antes de elegir un plan.

Qué ocurre: Los servicios web del nivel gratuito se suspenden después de 15 minutos de inactividad. La siguiente petición HTTP despierta el contenedor. Durante el proceso de activación, Render debe:

  1. Obtener la imagen Docker (en caché, rápido)
  2. Iniciar el contenedor
  3. Esperar a que tu app supere su health check

Para una API .NET típica, esto tarda 25–60 segundos. El inicio de ASP.NET no es rápido — escanea ensamblados, inicializa DI, calienta EF Core y conecta a Postgres antes de aceptar tráfico.

La primera petición después de un cold start o bien cuelga durante 30–60 segundos o devuelve un 502 (si el timeout del health check es menor que el tiempo de inicio de tu app).

Midiendo Tu Cold Start

Agrega timing de inicio para entender dónde se invierte el tiempo:

// Program.cs
var sw = System.Diagnostics.Stopwatch.StartNew();
 
var builder = WebApplication.CreateBuilder(args);
// ... registro de servicios ...
 
var app = builder.Build();
// ... pipeline de middleware ...
 
app.Lifetime.ApplicationStarted.Register(() =>
{
    sw.Stop();
    // Log a stdout — visible en el visor de logs de Render
    Console.WriteLine($"[STARTUP] Listo en {sw.ElapsedMilliseconds}ms");
});
 
await app.RunAsync();

Soluciones al Problema de Cold Start

Opción 1 — Ping externo de health check (gratuito)

Usa un monitor de uptime gratuito (UptimeRobot, Better Uptime, nivel gratuito de Cronitor) para hacer ping a tu endpoint /health cada 10 minutos. Esto mantiene el contenedor activo sin necesidad de actualizar.

Limitaciones: el propio monitor puede tener intervalos sin actividad, y los pings no cuentan como tráfico de usuarios — el temporizador de inactividad de Render se basa en peticiones HTTP de clientes externos, no de tu propia monitorización.

Opción 2 — Actualizar a Starter ($7/mes)

El plan Starter elimina los cold starts por completo. El servicio se ejecuta continuamente, y obtienes 512 MB de RAM y una CPU compartida. Para la mayoría de las APIs .NET, esta es la elección correcta en el momento en que tienes usuarios reales.

Opción 3 — Optimizar el tiempo de inicio

Reduce la duración del cold start aplazando la inicialización costosa:

// Usar IHostedService para inicialización en segundo plano en lugar de bloquear el inicio
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)
    {
        // Ejecutar init costoso en segundo plano — no bloquea el health check
        _ = Task.Run(() => _service.WarmUpAsync(ct), ct);
    }
 
    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
⚠️

Si tu app tarda más que el timeout del health check de Render (por defecto 180 segundos) en iniciar, el deploy fallará y Render revertirá a la versión anterior. Aumenta el timeout en Service Settings → Health Check Timeout, u optimiza el inicio.

Precios

PlanPrecio/mesRAMCPUCold StartsIdeal Para
Free$0512 MBCompartidaSí (15 min inactivo)Demos, proyectos personales
Starter$7512 MBCompartidaNoProyectos personales, bajo tráfico
Standard$252 GB1 vCPUNoAPIs en producción
Pro$854 GB2 vCPUNoServicios de alto tráfico
Pro Plus$1758 GB4 vCPUNoCargas de trabajo con uso intensivo de CPU
Selector de Instance Type de Render con el nivel Free seleccionado a $0 al mes con 512 MB de RAM y 0.1 CPU, junto a los niveles de pago: Starter $7 con 512 MB y 0.5 CPU, Standard $25 con 2 GB y 1 CPU, Pro $85 con 4 GB y 2 CPU, hasta Pro Ultra $450. Una nota advierte que las instancias gratuitas se suspenden tras períodos de inactividad.
El selector de instancias durante la configuración — los mismos precios de la tabla de arriba, directo del dashboard, con la advertencia de suspensión adjunta al nivel Free.

Precios de Postgres (separados del servicio web):

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

El Postgres gratuito expira 30 días después de su creación (sin backups) — después Render elimina la base de datos. Para datos gratuitos persistentes, usa un proveedor externo como Neon; para producción, usa al menos Basic-256.

Ancho de banda: 100 GB gratuitos al mes entre todos los servicios; $0.10/GB después.

Costo mensual estimado para una API .NET típica + Postgres:

  • Desarrollo: Web gratuito + Postgres gratuito = $0 (cold starts, límite de DB de 30 días)
  • Producción: Web Starter + Postgres Basic-256 = $14/mes
  • Producción escalada: Web Standard + Postgres Basic-1 = $53/mes

Benchmark: Render vs Railway (Números Reales)

Estos números se midieron de verdad (agosto de 2026) contra los dos despliegues activos de esta serie de artículos — la API .NET 10 de ejemplo en Render Free (Oregon) y Railway Hobby (US East). Fly.io estaba planeado como tercera plataforma pero no está medido: ya no tiene tier gratuito y el registro requiere tarjeta de crédito.

Configuración de prueba:

  • Herramienta: k6, script en benchmark/k6-latency.js — ejecútalo contra tus propios despliegues
  • Carga: tasa de llegada fija de 30 req/s durante 60 segundos (~1,800 peticiones por plataforma), servicios calentados primero
  • Endpoint: /, un payload JSON pequeño, sin base de datos — esto mide la ruta HTTP de la plataforma, no el rendimiento de consultas
  • Cliente: una sola máquina en Bogotá, Colombia. Con una tasa de llegada fija, la distribución de latencia está dominada por la ruta de red entre el cliente y la región del proveedor — estos números te dicen lo que experimenta un usuario en esa ubicación, no una velocidad absoluta de la plataforma. Vuelve a ejecutar el script desde donde están tus usuarios.

Latencia en Caliente

ProveedorPlanRegiónp50p95p99Peticiones fallidas
RenderFree ($0)Oregon152 ms206 ms318 ms0 de 1,800
RailwayHobby ($5)US East181 ms220 ms312 ms0 de 1,801

La sorpresa: Render resultó más rápido en la mediana a pesar de que su origen está más lejos (Oregon vs US East). La razón es la red edge — Render está detrás de Cloudflare, que terminó TLS en Bogotá, mientras que el edge más cercano de Railway era Miami. En el p99 son indistinguibles. La conclusión honesta: a 30 req/s en los tiers más baratos, ambas plataformas van sobradas, y la geografía de tus usuarios importa más que el cómputo de la plataforma.

Cold Start (primera petición tras 22 minutos inactivo)

Proveedor1.ª petición2.ª petición3.ª petición
Render Free12.3 s0.63 s0.45 s
Railway Hobby0.69 s0.45 s0.47 s

Render Free se había suspendido y necesitó 12.3 segundos para arrancar el contenedor, iniciar .NET y pasar el health check. Ojo: esta minimal API es el mejor caso — sin grafo de DI, sin calentamiento de EF Core, sin conexión a base de datos. Una aplicación real queda más cerca del rango de 25–60 segundos descrito antes, y por eso el propio banner del dashboard de Render advierte retrasos de "50 segundos o más". Railway Hobby nunca duerme, así que su número "frío" es solo un handshake TLS nuevo.

Conclusiones clave:

  • Ambos tiers más baratos manejaron 30 req/s con cero peticiones fallidas y un p99 por debajo de 330 ms desde Sudamérica.
  • Las redes edge vencen a la proximidad del origen en la mediana: Render + Cloudflare fue más rápido desde Bogotá que la región de origen más cercana de Railway.
  • La verdadera diferencia del tier gratuito es el cold start: Render Free paga 12+ segundos tras cada período inactivo; Railway no tiene tier web gratuito, así que nada se duerme jamás.
  • Fly.io no está medido aquí — si tienes cuenta, despliega el mismo ejemplo con su fly.toml y ejecuta el mismo script.

Pros y Contras

Pros

  • render.yaml es excelente — la historia de infra-as-code más limpia de las tres plataformas. Define todo en un archivo, diferéncialo en PRs.
  • Postgres gestionado es de primera clase — backups diarios automáticos, recuperación point-in-time en planes superiores, inyección de connection string sin fricciones.
  • Entornos de previsualización de PR — cada PR obtiene una URL live con configuración aislada. Enorme para flujos de trabajo de QA.
  • Sin configuración de red — el service discovery interno simplemente funciona. Los workers pueden llamar a servicios web por nombre sin configuración adicional.
  • Precios transparentes — sin sorpresas en la factura de egress. 100 GB/mes de ancho de banda gratuito cubre la mayoría de las apps pequeñas.
  • La experiencia de desarrollo está pulida — los logs de deploy son en tiempo real, los rollbacks son con un clic, los diffs de variables de entorno son auditables.

Contras

  • Los cold starts del nivel gratuito son brutales para .NET — activaciones de 30–60 segundos son peores que la mayoría de las plataformas debido a la sobrecarga de inicio de .NET. Aceptable para demos, inutilizable para usuarios reales.
  • Los tiempos de build dependen de tu Dockerfile — estructúralo para aprovechar la caché de capas (restore antes de copy) o lo notarás en cada deploy.
  • Sin edge/multi-región — Render tiene 4 regiones pero sin enrutamiento geo-automático ni distribución a nivel CDN. Fly.io gana aquí.
  • CPU compartida en Starter es limitada — las apps .NET con tareas intensivas en CPU (generación de PDF, procesamiento de imágenes) se verán throttleadas en Starter y necesitarán Standard ($25).
  • Sin soporte de GPU — no es una plataforma para cargas de trabajo de inferencia de ML.
  • El Postgres gratuito expira a los 30 días — fácil de olvidar, doloroso cuando tu base de datos desaparece en producción.

Cuándo Elegir Render

EscenarioVeredicto
Proyecto personal / demoEl nivel gratuito está bien — acepta los cold starts
Herramienta interna, bajo tráficoStarter ($7) — mejor relación calidad-precio
API en producción, tráfico moderadoStandard ($25) — elección sólida
Necesitas latencia más cercana al usuarioConsidera Fly.io en su lugar
Sin tarjeta de crédito / presupuesto de $0 estrictoRender Free es la única opción real de las tres
Infra-as-code completa en un archivoRender gana con render.yaml
Postgres gestionado con backupsRender es excelente
Entornos de previsualización de PRRender es el mejor de los tres

Resumen

Render.com es una excelente opción para desarrolladores .NET que quieren una experiencia PaaS limpia sin gestionar infraestructura. El formato render.yaml es la mejor historia de infra-as-code de cualquier plataforma comparable, el Postgres gestionado es genuinamente bueno, y los previews de PR son un multiplicador de flujo de trabajo.

El nivel gratuito solo es adecuado para demos y desarrollo — el problema de cold start con .NET es demasiado severo para cualquier cosa orientada al usuario. Presupuesta $14/mes (Starter + Basic Postgres) como mínimo para una app en producción. A ese precio, Render es difícil de superar por la combinación de experiencia de desarrollo, fiabilidad y servicios gestionados.

Si la latencia bruta es tu principal restricción, ejecuta el script de k6 de arriba desde la región de tus usuarios — nuestras mediciones muestran que la geografía y las redes edge dominan a esta escala. Pero para la mayoría de los equipos .NET que despliegan una API web o un worker en segundo plano, Render ofrece todo lo que necesitas en un solo lugar.

Lecturas adicionales

Sobre el autor

Jorge Calderón

Ingeniero de software con más de una década construyendo y operando aplicaciones .NET en producción — capas de datos con EF Core, servicios intensivos en async y despliegues en Azure y contenedores. Cada benchmark y proyecto de ejemplo de estas guías está publicado en un repositorio público de GitHub para que puedas reproducirlo.

Perfil de GitHubLinkedIn ↗Benchmarks y código de ejemplo

Artículos relacionados