tutorial

Server-Side Swift (Vapor) Uptime Monitoring Tutorial

Vapor apps crash, memory pressure grows, and dependencies go down — but Swift's type safety doesn't protect you from runtime failures. Learn how to add a /health endpoint to your Vapor app and configure Vigilmon for production uptime monitoring.

Server-side Swift with Vapor is a joy to write — the type system catches an entire class of bugs at compile time, and performance on Linux is genuinely impressive. But type safety doesn't prevent your app from crashing in production, running out of memory, or losing connectivity to its database. External monitoring is just as important for a Vapor app as for any other server.

I've shipped several Vapor services to production, and one thing I've noticed: the Swift/Vapor ecosystem has less tooling around observability than Node or Python. There are fewer off-the-shelf health check libraries, fewer monitoring tutorials, and fewer examples of production-ready health endpoints. This tutorial fills that gap.

I'll walk through adding a /health route to your Vapor application and configuring Vigilmon to monitor it — covering HTTP probes, dependency health checks, and alert configuration.


Why Your Vapor App Needs a /health Endpoint

Vapor's router can return 404 for undefined routes and the app will still be "running" from the OS perspective. A health endpoint does something the OS can't: it verifies that the app's event loop is processing requests, that its dependencies are reachable, and that its internal state is consistent.

A good /health endpoint:

  • Responds in under 100ms under normal conditions
  • Returns 200 OK with a JSON body when healthy
  • Returns 503 Service Unavailable when a critical dependency is down
  • Doesn't expose sensitive internal state
  • Is excluded from authentication middleware

Step 1: Add a /health Route to Your Vapor App

Basic Health Check

Start with the simplest version: a route that proves the event loop is alive.

// Sources/App/routes.swift
import Vapor

func routes(_ app: Application) throws {
    // ... your other routes ...
    
    app.get("health") { req async throws -> Response in
        let body: [String: String] = ["status": "ok", "service": "my-vapor-app"]
        return try Response(
            status: .ok,
            headers: HTTPHeaders([("Content-Type", "application/json")]),
            body: .init(data: JSONEncoder().encode(body))
        )
    }
}

This is the minimum viable health endpoint. It proves:

  • The Vapor process is running
  • The HTTP server is accepting connections
  • The event loop is processing requests

Health Check with Database Connectivity

If your app depends on a PostgreSQL database (via Fluent), extend the health endpoint to verify database connectivity:

// Sources/App/routes.swift
import Vapor
import Fluent

struct HealthResponse: Content {
    var status: String
    var database: String
    var timestamp: String
}

func routes(_ app: Application) throws {
    app.get("health") { req async throws -> Response in
        var dbStatus = "ok"
        
        // Test database connectivity with a lightweight query
        do {
            _ = try await req.db.query(SQLRawExecute("SELECT 1"))
        } catch {
            dbStatus = "down"
        }
        
        let overall = dbStatus == "ok" ? "ok" : "degraded"
        let statusCode: HTTPResponseStatus = overall == "ok" ? .ok : .serviceUnavailable
        
        let response = HealthResponse(
            status: overall,
            database: dbStatus,
            timestamp: ISO8601DateFormatter().string(from: Date())
        )
        
        return try Response(
            status: statusCode,
            headers: HTTPHeaders([("Content-Type", "application/json")]),
            body: .init(data: JSONEncoder().encode(response))
        )
    }
}

Health Check with Redis Cache

If you use Redis via the redis package:

import Vapor
import Redis

app.get("health") { req async throws -> Response in
    var redisStatus = "ok"
    
    do {
        let pong = try await req.redis.ping().get()
        if pong != "PONG" { redisStatus = "degraded" }
    } catch {
        redisStatus = "down"
    }
    
    let overall = redisStatus == "ok" ? "ok" : "degraded"
    let statusCode: HTTPResponseStatus = overall == "ok" ? .ok : .serviceUnavailable
    
    let body: [String: String] = [
        "status": overall,
        "redis": redisStatus,
    ]
    
    return try Response(
        status: statusCode,
        headers: HTTPHeaders([("Content-Type", "application/json")]),
        body: .init(data: JSONEncoder().encode(body))
    )
}

Exclude Health from Authentication Middleware

If you use authentication middleware globally, make sure the /health route bypasses it. In Vapor, this means registering the health route before applying middleware:

// Sources/App/configure.swift
public func configure(_ app: Application) async throws {
    // Register health endpoint BEFORE auth middleware
    app.get("health") { req async throws -> Response in
        // ... health check logic ...
    }
    
    // Auth middleware applies to everything else
    let protected = app.grouped(UserAuthMiddleware())
    try routes(protected)
}

Vigilmon's probes don't carry authentication headers, so an authenticated health route will always return 401 and appear down.


Step 2: Verify the Health Endpoint

Before wiring up Vigilmon, verify the endpoint manually:

# Basic health check
curl -i https://your-vapor-app.example.com/health

# Expected response:
# HTTP/2 200
# content-type: application/json
# {"status":"ok","database":"ok","timestamp":"2026-06-30T12:00:00Z"}

Test failure conditions too:

# Simulate DB down by temporarily setting an invalid DB URL in your dev environment
# Then verify /health returns 503
curl -i https://your-vapor-app.example.com/health
# HTTP/2 503
# {"status":"degraded","database":"down","timestamp":"..."}

Step 3: Configure Vigilmon HTTP Monitor for Your Vapor App

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL: https://your-vapor-app.example.com/health
  4. Set the check interval: 1 minute
  5. Under Expected response:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 2000ms
  6. Under Alert channels, assign your Slack channel or PagerDuty integration
  7. Save

Vigilmon probes your endpoint from multiple geographic regions simultaneously. It uses multi-region consensus before opening an incident — a single slow probe from one region won't page you, but a genuine Vapor failure that all regions see will.

What This Monitors

| Vapor failure | Detected? | |---|---| | Process crash (Vapor exits) | ✓ — connection refused | | Event loop blocked | ✓ — response time threshold exceeded | | Database connectivity lost | ✓ — returns 503 | | Redis down | ✓ — returns 503 | | Deployment failure (no process) | ✓ — connection refused | | Memory pressure causing slow responses | ✓ — response time threshold |


Step 4: Monitor Individual API Endpoints

Beyond the /health route, I recommend monitoring at least one representative business endpoint to catch routing misconfigurations, middleware failures, and partial startup failures.

In Vigilmon, add a second monitor:

  1. URL: https://your-vapor-app.example.com/api/status (or any lightweight GET endpoint)
  2. Expected status: 200
  3. Response time threshold: 3000ms
  4. Check interval: 5 minutes (less frequent than the health endpoint)

This catches cases where the health endpoint passes but business routing is broken — for example, a middleware registered after the health route that's crashing all other requests.


Step 5: Alert Configuration for Vapor App Downtime

Configure alert channels in Vigilmon to match your on-call structure:

Production Vapor app → Slack #backend-alerts + PagerDuty (immediate page)
Staging environment → Slack #backend-alerts only (no page)
Individual API endpoints → Slack #backend-alerts (5-minute delay)

For response time alerting, Vapor should respond to health checks in under 50ms under normal load. Set your threshold at 2 seconds for a confident signal that something is wrong — not just a slow GC pause.


Step 6: Heartbeat Monitoring for Background Jobs

Vapor apps often run background jobs (periodic tasks, queue workers, scheduled jobs using Queues). These jobs can stall silently — the Vapor process stays running, the health endpoint returns 200, but background processing has stopped.

Add Vigilmon heartbeat pings to your background jobs:

// Sources/App/Jobs/ReportGenerationJob.swift
import Vapor
import Queues
import AsyncHTTPClient

struct ReportGenerationJob: AsyncJob {
    typealias Payload = ReportRequest
    
    func dequeue(_ context: QueueContext, _ payload: ReportRequest) async throws {
        // ... do the work ...
        try await generateReport(payload)
        
        // Ping Vigilmon heartbeat after successful job completion
        let heartbeatURL = Environment.get("VIGILMON_HEARTBEAT_URL") ?? ""
        if !heartbeatURL.isEmpty {
            let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
            _ = try? await httpClient.get(url: heartbeatURL).get()
        }
    }
    
    func error(_ context: QueueContext, _ error: Error, _ payload: ReportRequest) async throws {
        // Don't ping on error — let the heartbeat timeout fire
        context.logger.error("Report generation failed: \(error)")
    }
}

Set up the heartbeat monitor in Vigilmon:

  1. Monitors → New Monitor → Heartbeat
  2. Name: vapor-report-generation-worker
  3. Expected interval: your job's maximum expected runtime + a buffer
  4. Grace period: 2x the expected interval

If your job stops running — because of a queue connection issue, a crash in the job runner, or an unhandled error that stops the queue consumer — the heartbeat timeout fires and you get a Vigilmon alert before any user notices.


Summary

Vapor's type safety makes your code more reliable at compile time, but it can't prevent runtime failures, dependency outages, or deployment issues. Here's the complete external monitoring setup:

| Monitor Type | What It Catches | |---|---| | HTTP probe on /health | Process crash, database connectivity, Redis health | | HTTP probe on a business endpoint | Routing failures, middleware crashes | | Heartbeat monitor | Background job and queue worker stalls |

Multi-region consensus in Vigilmon means confident, low-noise alerts — you get paged when your Vapor app is genuinely down, not when a single probe has a transient timeout.

Start monitoring free at vigilmon.online — your first Vapor monitor is running in under two minutes.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →