Repository navigation
Mitigate Render free-tier cold starts - #63
Conversation
The backend spins down after inactivity on Render's free tier, so the first request after a quiet period pays a multi-second JVM cold start. This addresses it on the free tier (no paid plan) two ways: - Hide it: add a lightweight, dependency-free GET /api/health endpoint and have the frontend ping it on app load (fire-and-forget, no-cors). The instance starts warming while the visitor is still on the landing page, so the cold start is absorbed before their first real request. - Shorten it: tune the JVM in the Dockerfile for faster startup on a small, short-lived instance (TieredStopAtLevel=1, SerialGC, lazy bean init, JMX off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for employee-management-fullstack-system canceled.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Code Review
This pull request introduces a backend liveness endpoint and a frontend warm-up utility to mitigate cold-start delays on free-tier hosting. It also tunes JVM flags in the Dockerfile to optimize startup performance. The feedback recommends removing lazy initialization to ensure the warm-up ping actually initializes the database pool and other essential beans, avoiding hardcoded localhost CORS origins on the health endpoint, and removing the hardcoded production URL fallback in the frontend utility to improve environment portability.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| # -Dspring.main.lazy-initialization=true defer bean creation until first use, so the context | ||
| # comes up faster (first request to each bean is slightly | ||
| # slower, which the frontend warm-up ping absorbs). | ||
| # -Dspring.jmx.enabled=false skip JMX bean export we don't use, trimming a little more startup. | ||
| ENTRYPOINT ["java", "-XX:TieredStopAtLevel=1", "-XX:+UseSerialGC", "-Dspring.main.lazy-initialization=true", "-Dspring.jmx.enabled=false", "-jar", "app.jar"] |
There was a problem hiding this comment.
While -Dspring.main.lazy-initialization=true reduces the initial JVM startup time, it defers the initialization of most Spring beans (including controllers, services, repositories, and the database connection pool) until they are first requested.
Because the frontend warm-up ping only hits /api/health (which has no dependencies), only HealthController will be initialized during the warm-up. The first real user request (e.g., fetching employees or logging in) will still trigger the lazy initialization of the database connection pool (HikariCP), Hibernate/JPA, and the respective controllers/services, causing a noticeable delay for the user.
To ensure the warm-up ping actually absorbs the initialization cost of the entire application, consider removing -Dspring.main.lazy-initialization=true so that all beans are fully initialized when the application starts up.
# -Dspring.jmx.enabled=false skip JMX bean export we don't use, trimming a little more startup.
ENTRYPOINT ["java", "-XX:TieredStopAtLevel=1", "-XX:+UseSerialGC", "-Dspring.jmx.enabled=false", "-jar", "app.jar"]
| */ | ||
| @RestController | ||
| @RequestMapping("/api/health") | ||
| @CrossOrigin(origins = "http://localhost:3000") |
There was a problem hiding this comment.
Hardcoding @CrossOrigin(origins = "http://localhost:3000") restricts CORS access to only local development. In production, if the frontend or any other client needs to read the health status (for example, to await warm-up completion and dismiss a loading spinner), the request will fail due to CORS.
Since this is a public, non-sensitive liveness endpoint that performs no DB or I/O operations, it is safe and more robust to allow all origins using @CrossOrigin (which defaults to all origins) or @CrossOrigin(origins = "*").
| @CrossOrigin(origins = "http://localhost:3000") | |
| @CrossOrigin |
| // the visitor is still reading the landing page — so by the time they log in or load data the | ||
| // instance is already up, and the cold start is hidden instead of felt. | ||
|
|
||
| const API_BASE = process.env.REACT_APP_API_BASE_URL || 'https://employee-management-app-gdm5.onrender.com'; |
There was a problem hiding this comment.
Hardcoding a specific production URL (https://employee-management-app-gdm5.onrender.com) as a fallback couples the codebase to a specific deployment instance. This reduces portability and can lead to unexpected behavior (e.g., development or staging environments sending warm-up requests to the production backend).
It is better to default to an empty string '' (relative path) or handle the absence of the environment variable gracefully.
| const API_BASE = process.env.REACT_APP_API_BASE_URL || 'https://employee-management-app-gdm5.onrender.com'; | |
| const API_BASE = process.env.REACT_APP_API_BASE_URL || ''; |
The healthcheck targeted /actuator/health, but Spring Boot Actuator isn't a dependency, so the probe always failed and the backend never reported healthy (blocking the frontend's depends_on: service_healthy). Use the lightweight /api/health endpoint added in this branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
The Spring Boot backend is hosted on Render's free tier, which spins the instance down after ~15 min of inactivity. The first request after a quiet period then pays a multi-second JVM cold start. Paid plans (which don't spin down) are out of scope — this solves it on the free tier, and within a shared instance-hour budget across multiple services (so 24/7 keep-alive pinging isn't viable).
Strategy: hide the cold start by warming on demand, and shorten it so the unavoidable cases hurt less.
What changed
Hide it — warm on page load
GET /api/healthendpoint (HealthController) — does no DB/IO, just returns{"status":"UP"}. Public via the existinganyRequest().permitAll().utils/warmup.js) fires a fire-and-forgetno-corsping to it once when the app mounts (wired inApp.js). The instance starts warming while the visitor is still reading the landing page, so it's up by their first real request. Costs Render hours only when there's actual traffic — nothing when idle.Shorten it — JVM startup tuning (
backend/Dockerfile)-XX:TieredStopAtLevel=1— skip slow C2 JIT we don't benefit from on a short-lived process-XX:+UseSerialGC— lowest startup/footprint overhead for a small heap-Dspring.main.lazy-initialization=true— defer bean creation so the context comes up faster (first-use cost absorbed by the warm-up ping)-Dspring.jmx.enabled=false— skip unused JMX exportNotes
HealthControllerfollows existing controller conventions. Frontendwarmup.jspassesnode --check.🤖 Generated with Claude Code