Skip to content

Mitigate Render free-tier cold starts - #63

Merged
hoangsonww merged 2 commits into
masterfrom
feat/render-coldstart-mitigation
Jun 12, 2026
Merged

hoangsonww merged 2 commits into
masterfrom
feat/render-coldstart-mitigation

Conversation

@hoangsonww

Copy link
Copy Markdown
Owner

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

  • New lightweight GET /api/health endpoint (HealthController) — does no DB/IO, just returns {"status":"UP"}. Public via the existing anyRequest().permitAll().
  • Frontend (utils/warmup.js) fires a fire-and-forget no-cors ping to it once when the app mounts (wired in App.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 export

Notes

  • No paid-plan dependency; no always-on pinger (respects the shared free-tier hour budget).
  • Backend compiled in Docker/CI (no local JDK in this environment); HealthController follows existing controller conventions. Frontend warmup.js passes node --check.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings June 12, 2026 02:17
@netlify

netlify Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for employee-management-fullstack-system canceled.

Name Link
🔨 Latest commit 82bd630
🔍 Latest deploy log https://app.netlify.com/projects/employee-management-fullstack-system/deploys/6a2b717c5481df0008b90c43

@vercel

vercel Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
employee-management-fullstack-app Ignored Ignored Jun 12, 2026 2:39am

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread backend/Dockerfile
Comment on lines +46 to +50
# -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"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 = "*").

Suggested change
@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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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>
@hoangsonww hoangsonww self-assigned this Jun 12, 2026
@hoangsonww hoangsonww added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request help wanted Extra attention is needed good first issue Good for newcomers dependencies Pull requests that update a dependency file labels Jun 12, 2026
@hoangsonww hoangsonww added this to the v2.x.x - Stable Release milestone Jun 12, 2026
@hoangsonww
hoangsonww merged commit 1b62f0a into master Jun 12, 2026
12 checks passed
@hoangsonww hoangsonww moved this from Done to In review in Employee Management App Project Board Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

2 participants