Skip to main content

Edge and S3 Response Cache

faynoSync can serve repeated version checks from object storage at the edge instead of hitting the API for every client. The design keeps the existing /checkVersion contract intact while moving the hot read path to SDK-driven, edge-first delivery backed by S3 (or S3-compatible) response manifests.

This page describes how that data plane works, how it relates to Performance Mode and Telemetry, and why an official SDK is part of the story.

Recommended for production

For any internet-facing instance, enable edge mode for public apps: set S3_BUCKET_NAME_CDN, turn on cdn for each public app, and set EdgeURL in the SDK. Steady-state update checks then never reach the API. See Production Deployment for the checklist.

Why this exists​

Even a well-tuned API eventually faces limits: connection churn, validation work, and database pressure scale with every client check. Pushing that load into clustering, replication, and heavier ops is not always what you want for an update server.

Edge mode changes the economics of the hot path. After warm-up, origin load is driven by distinct cache keys (version, channel, platform, architecture, and related dimensions), not by raw client count. One hundred thousand clients on five client versions should produce on the order of five origin calls per dimension set after caches are warm—not one hundred thousand API round trips.

In practice, keys multiply across your matrix:

N ≈ client_versions × channel × platform × arch × updater × package

Example: five client versions across five dimension combinations is on the order of twenty-five origin calls to build coverage—not one hundred thousand direct checks.

Two supported modes​

faynoSync deliberately supports both paths. You do not have to migrate every client at once.

ModeHow clients checkBest for
Dynamic APIGET /checkVersion on the API originExisting integrations, scripts, and tools that call the API directly
Edge + S3 cacheOfficial SDK with EdgeURL → cached manifests on object storageHigh-volume desktop, server, and auto-update clients that can use SDK edge-first behavior

Dynamic API behavior is unchanged. Edge mode adds a parallel, cache-friendly read path without forking the core update API.

How edge mode fits together​

What gets enabled​

Edge response caching is per application, via the cdn flag when you create or update an app (see Create Application and Update application).

Private apps cannot enable cdn: manifests are public objects and would expose a private app's versions and links. The API rejects the combination with 400.

Supporting infrastructure:

  • A public object storage bucket for response manifests (metadata only—binaries stay on the existing artifact flow). Configure S3_BUCKET_NAME_CDN as described in Environment Variables Overview.
  • An official faynoSync SDK configured with BaseURL (API origin) and optionally EdgeURL (edge/CDN base). Edge-first orchestration, fallback, cache fill, and telemetry integration are built for SDK clients—not for ad-hoc /checkVersion callers alone. See the Go SDK, JavaScript SDK and SDK overview.

Edge caching is independent from PERFORMANCE_MODE. Performance Mode uses Redis at the API layer; edge mode stores full JSON responses in object storage. You can run one, the other, or both.

Request flow (conceptual)​

SDK Client (`BaseURL` + optional `EdgeURL`)
↓
If `EdgeURL` is set, SDK checks edge/S3 first
↓
├─ HIT → return cached response to client
└─ MISS → call Origin API (`/checkVersion`) via `BaseURL`
↓
API returns normal response
↓
API writes the response to the S3 cache path in the background
↓
Future checks → edge/S3 HIT

When EdgeURL is set, the SDK asks the edge path first.

  • HIT — A manifest object exists for the request dimensions. The SDK returns the cached JSON body to the application.
  • MISS — No manifest (or not valid). The SDK calls /checkVersion on BaseURL and returns that response. The API writes the same response to S3_BUCKET_NAME_CDN in the background, so later checks become HITs. Clients never need write access to the bucket.

The API remains the source of truth. Edge storage is a derived, disposable cache of responses the API already knows how to produce.

Manifest layout and content​

Each cached file is the full faynoSync JSON response for that client context—the same fields the Dynamic API would return (update_available, package-specific update_url_* keys, changelog, critical, is_intermediate_required, possible_rollback, and so on).

Objects are stored at:

/responses/{owner}/{app_name}/{channel}/{platform}/{arch}/{updater}/{client_version}.json

Empty dimensions are left out of the path. The official SDKs check with updater=manual, so their manifests live under .../{arch}/manual/{client_version}.json.

There is intentionally no latest.json on the CDN. Version comparison stays on the server (or in precomputed per–client-version manifests). A single “latest” object at the edge would push comparison logic to clients and break faynoSync’s model of server-side update decisions.

Cached objects use standard cache headers (for example, Cache-Control: public, max-age=60, must-revalidate) so CDNs and browsers can revalidate frequently while still serving hot paths cheaply.

Publish, unpublish, and cache invalidation​

When you upload, publish, unpublish or delete any version for an app, faynoSync deletes response manifests for that application’s scope on object storage. A manifest write that was already in flight when the invalidation started is dropped, so a stale response cannot reappear after the sweep. The next checks for affected dimensions are MISSes; the SDK falls back to /checkVersion, the API repopulates S3, and subsequent traffic HITs again. The manifest is written in the background after the API has answered, so a request arriving within the next fraction of a second may still MISS.

Deleting a manifest removes it from the bucket, not from caches in front of it. A CDN, or Google Cloud Storage's built-in cache for public objects, can keep serving a deleted or overwritten manifest until its max-age (60 seconds) runs out. Plan for up to a minute between a publish and every client seeing it.

You do not need a separate warm-up job for every new channel or architecture combination—rare keys fill naturally through the fallback path the first time something asks for them.

Updater-specific behavior​

For default JSON update checks, the cached body is the ordinary faynoSync response. “No update” responses are cached under the same key rules as “update available” responses.

Updater-specific contracts (for example Electron Builder, Tauri, or Squirrel on Windows) still receive the payloads the API already produces; only the lookup path changes from “always origin” to “edge first, origin on MISS.” The JavaScript SDK maps cached JSON to updater-facing HTTP semantics (such as 204 No Content or redirect-style responses) where those integrations require it.

Performance Mode vs edge cache​

AspectPerformance ModeEdge + S3 cache
Where it cachesRedis at the APIObject storage manifests at the edge
Who benefitsAny client calling /checkVersionSDK clients with EdgeURL and app cdn enabled
Cache keyAPI request signaturePer-dimension manifest path
InvalidationRedis TTL / cache policiesPublish/unpublish clears app manifest scope
CouplingPERFORMANCE_MODE=trueApp cdn + SDK + S3_BUCKET_NAME_CDN

Use Performance Mode to shave latency and DB load on the origin API. Use edge mode when check volume is dominated by many clients repeating the same dimension keys and you want the origin to see MISS traffic only, not every poll.

Telemetry in edge flows​

Today, much telemetry is collected inside /checkVersion when clients send X-Device-ID. In edge mode, many checks never reach that endpoint, so statistics would silently drop if nothing changed.

Before and after​

Classic path (API-only clients):

Client → (optional X-Device-ID) telemetry inline → /checkVersion

Edge path (SDK clients):

Client → edge manifest HIT (no /checkVersion)
→ (optional X-Device-ID) → /telemetry/beacon on API

SDK-integrated clients that skip /checkVersion should use /telemetry/beacon so device and dimension stats stay accurate. If a client still calls /checkVersion on every check, it does not need a separate beacon call.

See Telemetry System for dashboard behavior, retention, and ENABLE_TELEMETRY.

How the beacon stays fast​

/checkVersion still performs meaningful validation even when Performance Mode is on. The beacon endpoint is optimized for volume: it avoids per-request database lookups against full apps_meta documents.

Instead, the API maintains a compact in-memory allow-list index built from names only when metadata changes:

  • owners
  • applications
  • channels
  • platforms
  • architectures
  • versions (for index structure—not used to reject reported client versions)

Changelog, artifacts, and other heavy fields stay out of memory. On each relevant change (app created, channel added, version published, and so on), the service rebuilds the index from a projection and atomically swaps the in-memory pointer handlers use.

Beacon validation checks that (owner, app, channel, platform, arch) exists in the index. Reported client version is recorded but not required to match the allow-list—clients may still report a version removed in the admin UI. Invalid dimension combinations are rejected cheaply (success responses remain lightweight, typically 204 No Content). Team-user scenarios are out of scope for this public beacon path.

Counters still flow to Redis the same way as today’s telemetry pipeline, so dashboards remain meaningful under edge load.

Multi-instance deployments can add periodic index reload later; single-node setups rely on rebuild-on-change.

Internal benchmarks (local development)​

These numbers were measured on a local Mac during development. Production servers will report different absolute values depending on hardware, network, Redis, and deployment size. The comparison still reflects the design goal: validate dimensions from an in-memory snapshot instead of hitting the database on every beacon.

What was measured: elapsed time from handler start until the statistics persistence step is invoked (average over many requests).

ApproachAverage time to persistence
Previous path (DB-backed validation per request)~20.8 ms
Beacon path (in-memory allow-list snapshot)~0.15 ms

Load test: wrk with 4 threads, 100 connections, 30 seconds, same local machine.

ApproachRequests/secAvg latency
Previous path~359~287 ms
Beacon path~1,192~88 ms

Under this setup, the beacon path handled roughly 3× more requests per second with lower average latency. Treat these figures as order-of-magnitude guidance, not a SLA or capacity guarantee.

Operational goals​

  1. SDK-first — No separate CDN application is required; clients implement edge-first behavior through the SDK.
  2. Per-app control — Turn cache mode on or off per application with cdn.
  3. Stable cache keys — Deterministic manifest paths per owner, app, channel, platform, arch, and client version.
  4. Safe lifecycle — Publish and unpublish clear cached manifests for the app scope.
  5. Hot-path offload — Edge serves repeated checks; API serves MISS, control plane, and fallback fill.