All writing
5 min

Inside the NextDeploy Go Sidecar: In-Process Caching, ISR, and Image Pipeline

An architectural deep-dive into the NextDeploy Go Sidecar: embedded within nextdeployd, loopback interceptor, atomic CAS ISR revalidation, and anti-SSRF image optimization.


In a standard Next.js standalone deployment, the Node.js process is burdened by tasks ill-suited for a single-threaded V8 runtime: serving immutable pre-rendered HTML, reading React Server Component (.rsc) Flight buffers from disk, executing image transforms via Sharp wrappers, and managing ISR timers.

NextDeploy addresses this with an embedded Go Sidecar. Rather than introducing a separate binary or additional systemd service, the sidecar is an optional, in-process HTTP engine compiled directly into the existing nextdeployd daemon (daemon/internal/daemon/sidecar*.go).

Zero new binaries, zero extra systemd units, zero extra CLI commands. The sidecar runs inside nextdeployd on a release-scoped loopback port between Caddy and Node.js.

1. Opt-In Configuration & Request Topology

The sidecar is disabled by default. You enable it in nextdeploy.yml under app.gosidecar:

yaml· 8 lines
1app:
2  name: my-app
3  port: 3000
4  gosidecar:
5    enabled: true              # true | false (default: false)
6    isr_cache: true            # Serve SSG/ISR HTML & .rsc from Go (default: true)
7    image_optimization: true   # Intercept /_next/image in Go (default: true)
8    max_image_workers: 4       # Concurrency semaphore (default: 4, memory-aware)

When enabled, the network topology adapts as follows:

Diagram · System Flow
Rendering diagram...

2. Real Wire Protocol & Diagnostic Headers

Every response processed by the Go sidecar injects explicit diagnostic headers grounded in daemon/internal/daemon/sidecar.go:

  • X-NextDeploy-Sidecar: go Injected on every response handled by the sidecar listener.
  • X-NextDeploy-Handler: Identifies the subsystem that served the response: go-html, go-rsc, go-image, or node.
  • X-NextDeploy-Cache: Indicates cache state: HIT (fresh cache hit), STALE (served stale while triggering background revalidation), or MISS-SSR (bypassed to Node.js).
  • X-NextDeploy-Fallback: When proxied to Node, provides the exact programmatic reason: middleware, request-not-cacheable, image-disabled, query-string, not-prerendered, image-config, image-format, or image-dimensions.
  • /_nextdeploy/sidecar-healthz: Dedicated sidecar liveness probe returning HTTP 200 OK without touching Node.js.

  • 3. Prerendered Routes & Traversal-Safe Ingestion

    During NewSidecarHandler initialization, the sidecar parses SSG and ISR route definitions from metadata.json. Files are read beneath the server output directory using Go's os.OpenRoot:

  • Strict Root Bounds: Rejects any symlink escaping the server distribution directory.
  • Bounded Ingestion: Reads are capped by io.LimitReader to maxRouteBody = 32MB to prevent memory exhaustion.
  • Dual Representations: HTML and .rsc buffers are held in memory with independent freshness timestamps (htmlUpdated, rscUpdated) and independent atomic flags.
  • Conditional Requests: Generates SHA-256 ETags and evaluates incoming If-None-Match headers, returning HTTP 304 Not Modified instantly.

  • 4. Incremental Static Regeneration (ISR) with Atomic CAS

    When an incoming GET request targets an expired ISR route, the sidecar executes bounded background revalidation (sidecar_isr.go):

  • 1. Immediate Stale Return: The sidecar serves the cached HTML payload immediately with X-NextDeploy-Cache: STALE and X-NextDeploy-Handler: go-html.
  • 2. Deduplication Lock: An atomic CompareAndSwap(false, true) on htmlReval ensures exactly one background goroutine revalidates the route. Duplicate requests bypass duplicate network traffic.
  • 3. Bounded Revalidation: A background HTTP GET request is dispatched to Node.js with a strict 30-second context timeout (context.WithTimeout).
  • 4. Atomic Update: If Node.js responds with 200 OK, the in-memory buffer is replaced, the timestamp is refreshed, and htmlReval is reset to false.
  • 5. Flight Guard: Unlike HTML, stale React Flight requests (.rsc) are never served stale. They immediately fall back to Node.js to ensure client RSC streams remain synchronized with active component trees.

  • 5. Native Go Image Optimization Pipeline

    When requests hit /_next/image?url=...&w=...&q=..., the sidecar processes the image in Go (sidecar_image.go) without invoking Node's libvips bindings:

  • Whitelist Validation: Rejects widths not explicitly declared in next.config deviceSizes or imageSizes, and enforces allowed qualities.
  • Memory-Aware Concurrency: A channel semaphore (imageSem) bounds concurrent resizing. Concurrency is calculated via memoryAwareImageWorkers based on available VPS memory.
  • Anti-SSRF Safe Dialer: When fetching remote image sources, dialPublicImageHost resolves DNS and verifies the target IP with netip. IsLoopback, IsPrivate, and IsLinkLocal are rejected, neutralizing Server-Side Request Forgery (SSRF).
  • On-Disk Caching: Checks disk cache at sha256(url + width + quality).jpg before acquiring an optimization slot. Hit responses stream directly with Cache-Control: public, max-age=TTL.
  • Safe JPEG Envelope: The Go optimizer compiles to a high-throughput JPEG encoder. Requests requiring transparent PNG preservation, animated WebP/GIF, EXIF metadata, or CMYK color spaces fall back to Node.js safely via X-NextDeploy-Fallback.

  • 6. Release Lifecycle & Cutover Orchestration

    The SidecarManager (sidecar_manager.go) manages release-scoped listeners. The sidecar port is stored at <release>/.nextdeploy/sidecar.port, while the Node port is stored at <release>/.nextdeploy/node.port.

    During activation, the candidate sidecar binds a new loopback port while the active release continues serving live traffic. Once Caddy reloads and the atomic symlink flips, the daemon calls StopRelease to cleanly drain in-flight revalidations, close idle keep-alive connections, and terminate the previous listener without dropping connections.


    7. Hands-On Verification & Wire Diagnostic Commands

    Validate your Go sidecar runtime and inspect wire protocol headers using curl:

    bash· 34 lines
    1# 1. Check sidecar daemon health probe
    2curl -i http://127.0.0.1:3000/_nextdeploy/sidecar-healthz
    3# Expected: HTTP/1.1 200 OK {"status":"healthy","subsystem":"gosidecar"}
    4
    5# 2. Inspect wire protocol headers on a prerendered SSG route
    6curl -I https://app.example.com/about
    7# Expected headers:
    8# X-NextDeploy-Sidecar: go
    9# X-NextDeploy-Handler: go-html
    10# X-NextDeploy-Cache: HIT
    11
    12# 3. Test Incremental Static Regeneration (ISR)
    13# Request 1 after TTL expiration:
    14curl -I https://app.example.com/products
    15# X-NextDeploy-Handler: go-html
    16# X-NextDeploy-Cache: STALE
    17# (Background CAS goroutine dispatches to Node.js)
    18
    19# Request 2 (immediately after revalidation completes):
    20curl -I https://app.example.com/products
    21# X-NextDeploy-Handler: go-html
    22# X-NextDeploy-Cache: HIT
    23
    24# 4. Verify Go Image Optimization Pipeline
    25curl -I "https://app.example.com/_next/image?url=/logo.png&w=640&q=75"
    26# X-NextDeploy-Sidecar: go
    27# X-NextDeploy-Handler: go-image
    28# Cache-Control: public, max-age=31536000, immutable
    29
    30# 5. Inspect dynamic fallback reason
    31curl -I -H "Cookie: session=xyz" https://app.example.com/account
    32# X-NextDeploy-Sidecar: go
    33# X-NextDeploy-Handler: node
    34# X-NextDeploy-Fallback: request-not-cacheable

    Measured efficiency over assumption: the Go Sidecar relieves Node.js of static and image I/O while preserving complete App Router compatibility.