Routing System

Sentinel’s routing system determines how incoming requests are matched to configured routes. This page covers match conditions, priority rules, and performance optimizations.

Overview

                     Incoming Request
                  ┌─────────────────┐
                  │  Route Matcher  │
                  │                 │
                  │  ┌───────────┐  │
                  │  │   Cache   │◀─┼── Cache hit? Return immediately
                  │  └───────────┘  │
                  │        │        │
                  │        ▼        │
                  │  ┌───────────┐  │
                  │  │ Compiled  │  │
                  │  │  Routes   │  │
                  │  │ (sorted)  │  │
                  │  └───────────┘  │
                  └────────┬────────┘
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
         Route A      Route B      Route C
         (pri:100)    (pri:50)     (pri:10)
              │            │            │
              │       ✓ First Match     │
              │            │            │
              └────────────┴────────────┘
                    Return RouteMatch

Route Configuration

Routes are defined in your configuration file:

routes {
    route "api-users" {
        priority 100

        matches {
            path-prefix "/api/users"
            method "GET" "POST" "PUT" "DELETE"
        }

        upstream "user-service"
        service-type "api"
    }

    route "static-assets" {
        priority 50

        matches {
            path-prefix "/static/"
        }

        service-type "static"
        static-files {
            root "/var/www/static"
        }
    }

    route "catch-all" {
        priority 1

        matches {
            path-prefix "/"
        }

        upstream "default-backend"
    }
}

Match Conditions

Routes can match on multiple criteria. All conditions must match (AND logic).

Path Matching

Exact Path

Matches the exact path string:

matches {
    path "/api/health"
}
Request PathMatch?
/api/healthYes
/api/health/No
/api/healthcheckNo

Path Prefix

Matches paths starting with the prefix:

matches {
    path-prefix "/api/"
}
Request PathMatch?
/api/Yes
/api/usersYes
/api/users/123Yes
/apiv2/usersNo

Path Regex

Matches paths against a regular expression:

matches {
    path-regex "/users/[0-9]+/profile"
}
Request PathMatch?
/users/123/profileYes
/users/456/profileYes
/users/abc/profileNo

Common regex patterns:

PatternDescription
/api/v[0-9]+/.*Versioned API paths
/users/[0-9]+Numeric user IDs
/.*/healthHealth endpoints at any level
/[a-z]{2}/.*Two-letter locale prefix

Host Matching

Match based on the Host header:

Exact Host

matches {
    host "api.example.com"
}

Wildcard Host

Matches subdomains:

matches {
    host "*.example.com"
}
Host HeaderMatch?
api.example.comYes
www.example.comYes
example.comNo
deep.sub.example.comNo (single level only)

Host Regex

For complex host patterns:

matches {
    host-regex "^(api|www)\\.example\\.(com|io)$"
}

Method Matching

Match specific HTTP methods:

matches {
    method "GET" "POST"
}
Request MethodMatch?
GETYes
POSTYes
PUTNo
DELETENo

Header Matching

Header Presence

Match if header exists (any value):

matches {
    header "Authorization"
}

Header Value

Match if header has specific value:

matches {
    header "X-Api-Version" value="2"
}
HeadersMatch?
X-Api-Version: 2Yes
X-Api-Version: 1No
(no header)No

Query Parameter Matching

Parameter Presence

matches {
    query-param "debug"
}
URLMatch?
/api?debug=trueYes
/api?debug=Yes
/api?debugYes
/api?other=valueNo

Parameter Value

matches {
    query-param "version" value="2"
}
URLMatch?
/api?version=2Yes
/api?version=1No

Combining Conditions

Multiple conditions are combined with AND logic:

route "admin-api" {
    matches {
        path-prefix "/admin/"
        method "GET" "POST"
        header "X-Admin-Token"
        host "admin.example.com"
    }
    upstream "admin-service"
}

This route only matches if:

  • Path starts with /admin/ AND
  • Method is GET or POST AND
  • X-Admin-Token header is present AND
  • Host is admin.example.com

Priority System

When multiple routes could match, priority determines the winner.

Priority Levels

route "high-priority" {
    priority 100    // Evaluated first
}

route "normal-priority" {
    priority 50     // Evaluated second
}

route "low-priority" {
    priority 10     // Evaluated last
}

Higher numbers = higher priority = evaluated first.

Named Priority Levels

You can also use named levels:

NameNumeric Value
critical1000
high100
normal50 (default)
low10
background1
route "critical-health" {
    priority critical
    matches { path "/-/health" }
}

Priority Example

Request: GET /api/users/123
         Host: api.example.com

Routes evaluated in order:
┌────────────────────────────────────────────────────────────┐
│ 1. route "api-user-detail" pri=100                         │
│    matches: path-regex "/api/users/[0-9]+"                 │
│    Result: ✓ MATCH → Selected!                             │
├────────────────────────────────────────────────────────────┤
│ 2. route "api-users" pri=80                                │
│    matches: path-prefix "/api/users"                       │
│    Result: (not evaluated - already matched)               │
├────────────────────────────────────────────────────────────┤
│ 3. route "api-catchall" pri=50                             │
│    matches: path-prefix "/api/"                            │
│    Result: (not evaluated - already matched)               │
└────────────────────────────────────────────────────────────┘

Specificity Tie-Breaking

When routes have the same priority, specificity breaks ties:

Specificity Scores:
┌─────────────────────────────────────┐
│ Match Type          │ Score         │
├─────────────────────┼───────────────┤
│ Exact path          │ 1000          │
│ Path regex          │ 500           │
│ Path prefix         │ 100           │
│ Host                │ 50            │
│ Header (with value) │ 30            │
│ Header (presence)   │ 20            │
│ Query param (value) │ 25            │
│ Query param (pres.) │ 15            │
│ Method              │ 10            │
└─────────────────────────────────────┘

Example:

// Both have priority 50

route "specific" {
    priority 50
    matches {
        path "/api/users"        // +1000
        method "GET"             // +10
    }
    // Total specificity: 1010
}

route "general" {
    priority 50
    matches {
        path-prefix "/api/"      // +100
    }
    // Total specificity: 100
}

For request GET /api/users, the “specific” route wins due to higher specificity.

Route Compilation

Routes are compiled at startup for efficient matching:

Configuration                    Compiled
┌──────────────────┐            ┌──────────────────────────┐
│ route "api" {    │            │ CompiledRoute {          │
│   matches {      │   ────▶    │   id: "api",             │
│     path-prefix  │            │   priority: 50,          │
│       "/api/"    │            │   matchers: [            │
│     method       │            │     PathPrefix("/api/"), │
│       "GET"      │            │     Method(["GET"]),     │
│   }              │            │   ],                     │
│ }                │            │   specificity: 110,      │
└──────────────────┘            │ }                        │
                                └──────────────────────────┘

What’s compiled:

  • Regex patterns are pre-compiled
  • Host wildcards are parsed
  • Routes are sorted by priority
  • Specificity scores are calculated

Route Cache

Sentinel caches route matches for performance:

┌───────────────────────────────────────────────────────────┐
│                     Route Cache                            │
│  ┌─────────────────────────────────────────────────────┐  │
│  │ Cache Key: "{method}:{host}:{path}"                 │  │
│  │                                                     │  │
│  │ "GET:api.example.com:/users/123" → route-id: "api"  │  │
│  │ "POST:api.example.com:/login"    → route-id: "auth" │  │
│  │ "GET:www.example.com:/about"     → route-id: "web"  │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                           │
│  Max Size: 1000 entries                                   │
│  Eviction: LRU (Least Recently Used)                      │
│                                                           │
└───────────────────────────────────────────────────────────┘

Cache behavior:

  1. Cache hit: Return route immediately (no evaluation)
  2. Cache miss: Evaluate all routes, cache the result
  3. Eviction: When full, remove least recently used entries
  4. Invalidation: Cache clears on configuration reload

When Caching Doesn’t Help

Cache is bypassed when requests vary significantly:

  • Random query parameters in cache key
  • Unique paths (e.g., UUIDs in path)
  • Many different hosts

Default Route

Configure a catch-all route for unmatched requests:

routes {
    // Specific routes first
    route "api" {
        priority 100
        matches { path-prefix "/api/" }
        upstream "api-service"
    }

    // Default route (lowest priority)
    route "default" {
        priority 1
        matches { path-prefix "/" }
        upstream "default-backend"
    }
}

Or specify a default route explicitly:

routing {
    default-route "fallback"
}

routes {
    route "fallback" {
        upstream "default-backend"
    }
}

No Match Behavior

When no route matches and no default is configured:

{
  "status": 404,
  "error": "no_route",
  "message": "No route matched request",
  "path": "/unknown/path",
  "trace_id": "abc-123"
}

Best Practices

1. Order by Specificity

Put more specific routes before general ones:

// Good: Specific first
route "user-profile" { priority 100; matches { path-regex "/users/[0-9]+/profile" } }
route "user-detail"  { priority 90;  matches { path-regex "/users/[0-9]+" } }
route "users-list"   { priority 80;  matches { path-prefix "/users" } }
route "api-catchall" { priority 50;  matches { path-prefix "/api/" } }

2. Use Exact Paths When Possible

Exact paths are faster and more predictable:

// Prefer this for known endpoints
route "health" {
    matches { path "/-/health" }
}

// Over regex for simple cases
route "health" {
    matches { path-regex "^/-/health$" }  // Slower
}

3. Limit Regex Complexity

Simple regexes match faster:

// Fast
matches { path-regex "/users/[0-9]+" }

// Slower (backtracking)
matches { path-regex "/users/.*?/profile/.*" }

4. Use Priority Gaps

Leave gaps for future routes:

system {
    worker-threads 0
}

listeners {
    listener "http" {
        address "0.0.0.0:8080"
        protocol "http"
    }
}

// Example priority values with gaps for future insertion
routes {
    route "critical" {
        priority 1000
        matches { path-prefix "/critical" }
        upstream "backend"
    }
    route "high" {
        priority 100    // Gap allows 101-999
        matches { path-prefix "/high" }
        upstream "backend"
    }
    route "normal" {
        priority 50     // Gap allows 51-99
        matches { path-prefix "/normal" }
        upstream "backend"
    }
    route "low" {
        priority 10     // Gap allows 11-49
        matches { path-prefix "/low" }
        upstream "backend"
    }
    route "default" {
        priority 1
        matches { path-prefix "/" }
        upstream "backend"
    }
}

upstreams {
    upstream "backend" {
        targets {
            target { address "127.0.0.1:3000" }
        }
    }
}

5. Avoid Overlapping Routes

Overlapping routes with same priority cause confusion:

// Avoid: Both could match /api/users, same priority
route "api-a" { priority 50; matches { path-prefix "/api/" } }
route "api-b" { priority 50; matches { path-prefix "/api/users" } }

// Better: Different priorities
route "api-users" { priority 60; matches { path-prefix "/api/users" } }
route "api-other" { priority 50; matches { path-prefix "/api/" } }

Debugging Routes

Test Route Matching

Use the CLI to test which route matches:

sentinel route-test --path "/api/users/123" --method GET --host api.example.com

Output:

Matched route: api-users
  Priority: 100
  Specificity: 610
  Upstream: user-service

Evaluated routes:
  1. api-users (pri=100, spec=610) ✓ MATCHED
  2. api-catchall (pri=50, spec=100) - (not evaluated)
  3. default (pri=1, spec=100) - (not evaluated)

View Compiled Routes

sentinel routes --compiled

Monitor Cache Performance

sentinel stats routes
Route Cache Statistics:
  Entries: 847/1000
  Hit Rate: 94.2%
  Evictions: 12,453

Top Cached Routes:
  1. api-users: 45,231 hits
  2. static-assets: 23,456 hits
  3. health: 12,345 hits

Performance Characteristics

OperationComplexityTypical Time
Cache lookupO(1)< 1μs
Route evaluation (no cache)O(n)10-100μs
Regex matchO(m)1-10μs per regex
Cache insertO(1)< 1μs
LRU evictionO(1)< 1μs

Where:

  • n = number of routes
  • m = path length

Next Steps