How to Monitor Your Apache FreeMarker Application (Free, Multi-Region)
Apache FreeMarker is a widely used Java template engine for generating HTML pages, transactional emails, XML configuration files, and source code. When the FreeMarker Configuration breaks — missing templates, corrupted classpath, or a bad deploy — your web pages go blank, your welcome emails stop sending, and your XML exports silently fail.
By the end of this guide you'll have external uptime monitoring, multi-region probes, heartbeat checks for background email and generation jobs, and instant alerts — all on the free tier.
Why FreeMarker-based apps go down silently
FreeMarker applications have a few failure modes that are easy to miss:
Template not found at render time — FreeMarker resolves templates lazily. A missing or misnamed .ftl file causes a TemplateNotFoundException only when that route is hit for the first time after a bad deploy — minutes or hours later.
Silent email batch failures — transactional email jobs use FreeMarker outside any HTTP request. When they crash mid-run, no HTTP 500 is emitted. The emails simply stop going out.
Configuration reload issues — Configuration.setTemplateUpdateDelaySeconds() controls hot-reloading. On some setups a stale file handle or permission problem causes the engine to serve cached but broken output without throwing.
Step 1: Expose a health endpoint that exercises the template engine
A health check that just returns {"status":"UP"} hardcoded is useless — you want to prove the FreeMarker engine actually works.
With Spring Boot (using Spring's FreeMarker auto-configuration):
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.StringWriter;
import java.util.Map;
@RestController
public class HealthController {
private final Configuration freeMarkerConfig;
public HealthController(Configuration freeMarkerConfig) {
this.freeMarkerConfig = freeMarkerConfig;
}
@GetMapping("/health")
public Map<String, String> health() throws Exception {
// Render a trivial inline template to verify the engine
Template tpl = new freemarker.template.Template(
"health",
new java.io.StringReader("${status}"),
freeMarkerConfig
);
StringWriter out = new StringWriter();
tpl.process(Map.of("status", "UP"), out);
return Map.of("status", "UP", "engine", "ok");
}
}
With a plain Java servlet:
import freemarker.template.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.Map;
@WebServlet("/health")
public class HealthServlet extends HttpServlet {
private Configuration cfg;
@Override
public void init() throws ServletException {
cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setClassForTemplateLoading(getClass(), "/templates");
cfg.setDefaultEncoding("UTF-8");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("application/json");
try {
Template tpl = new Template("hc", new StringReader("${ok}"), cfg);
StringWriter sw = new StringWriter();
tpl.process(Map.of("ok", "UP"), sw);
res.getWriter().write("{\"status\":\"UP\"}");
} catch (Exception e) {
res.setStatus(500);
res.getWriter().write("{\"status\":\"DOWN\",\"error\":\"" + e.getMessage() + "\"}");
}
}
}
Step 2: Set up external monitoring with Vigilmon
With /health live, connect it to Vigilmon:
- Sign up at vigilmon.online — free tier, no credit card
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval (5 minutes on free tier)
- Save
Vigilmon probes from multiple regions and alerts on the first failure — before your users report anything.
Monitor each critical surface separately:
| Endpoint | What it catches |
|---|---|
| /health | FreeMarker Configuration failure, engine errors |
| / | Home page template rendering |
| /api/generate | Core generation endpoint |
Step 3: Heartbeat monitoring for email and generation jobs
FreeMarker is heavily used for transactional and batch emails. These run as background jobs outside any HTTP cycle — there's nothing to probe from outside. Heartbeat monitoring covers this gap.
import freemarker.template.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
public class WelcomeEmailJob {
private final Configuration cfg;
private final String heartbeatUrl = System.getenv("HEARTBEAT_WELCOME_EMAIL_URL");
public WelcomeEmailJob(Configuration cfg) {
this.cfg = cfg;
}
public void sendPendingWelcomeEmails() throws Exception {
Template tpl = cfg.getTemplate("email/welcome.ftl");
for (NewUser user : getPendingUsers()) {
StringWriter body = new StringWriter();
tpl.process(Map.of(
"firstName", user.getFirstName(),
"activationLink", user.getActivationLink()
), body);
deliver(user.getEmail(), "Welcome to the app!", body.toString());
markSent(user.getId());
}
// Only ping Vigilmon after all emails sent successfully
if (heartbeatUrl != null && !heartbeatUrl.isEmpty()) {
pingHeartbeat(heartbeatUrl);
}
}
private void pingHeartbeat(String url) throws Exception {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5_000);
conn.getResponseCode();
conn.disconnect();
}
// ... getPendingUsers(), deliver(), markSent() omitted
}
In Vigilmon:
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. 65 minutes if the job runs hourly)
- Copy the unique ping URL
- Set the environment variable:
HEARTBEAT_WELCOME_EMAIL_URL=https://vigilmon.online/api/heartbeat/your-unique-token
If a TemplateNotFoundException, a rendering crash, or a network error stops the job mid-run, the heartbeat is never pinged, and you'll get an alert after one missed interval.
Step 4: Validate templates at startup
FreeMarker's lazy loading means a broken template only causes a 500 when that route is first requested. Force the issue at startup by preloading all critical templates:
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class FreemarkerTemplateValidator {
private final Configuration cfg;
private static final String[] REQUIRED = {
"layout/base.ftl",
"pages/home.ftl",
"email/welcome.ftl",
"email/password-reset.ftl",
"email/weekly-digest.ftl"
};
public FreemarkerTemplateValidator(Configuration cfg) {
this.cfg = cfg;
}
@EventListener(ApplicationReadyEvent.class)
public void validate() throws Exception {
for (String name : REQUIRED) {
Template tpl = cfg.getTemplate(name); // throws if missing
if (tpl == null) {
throw new IllegalStateException("Required FreeMarker template missing: " + name);
}
}
}
}
A missing template now fails the startup health check rather than surfacing as a runtime 500 hours after deploy.
Step 5: Alerts and badge embed
Slack/Discord alerts:
- In Vigilmon go to Notifications → New Channel
- Choose Slack or Discord, paste your webhook URL
- Enable it on your monitors
You get an instant alert when a monitor trips and a recovery notification when it comes back.
Add an uptime badge:
[](https://vigilmon.online?utm_source=devto&utm_medium=article&utm_campaign=freemarker-tutorial)
What you've built
| What | How |
|---|---|
| External health checks | /health endpoint + Vigilmon HTTP monitor |
| Engine validation | Live template render inside health check |
| Email job monitoring | Heartbeat ping after each successful batch run |
| Startup validation | ApplicationReadyEvent preloads critical templates |
| Instant alerts | Slack/Discord notifications |
| README status badge | Vigilmon badge embed |
The entire setup runs on the free tier in under 30 minutes. You'll catch template failures, engine configuration errors, and silently stopped email jobs before your users or customers notice.
Get started free at vigilmon.online — monitors running in under a minute, no credit card required.