Experiment record

Cloudflare Pages public access counter: D1 binding failure and recovery

A site-wide public counter for AI Experiment Log, including a production 503 caused by a missing D1 runtime binding, diagnosis through Pages Function logs, and recovery by recreating the binding and redeploying.

Conclusion

A small site-wide public access counter was added to the AI Experiment Log footer.

The final path is browser JavaScript -> /api/counter -> Cloudflare Pages Function -> D1. Japanese pages show アクセス数; English pages show Site visits.

Immediately after implementation, /api/counter returned HTTP 503 with counter_unavailable. Pages Function runtime logs identified the decisive failure: the running Function did not have the COUNTER_DB D1 binding. After the binding was recreated in Cloudflare Pages settings and the project was redeployed, the counter was confirmed to increment normally.

The directly verified failure was therefore not D1 SQL; execution stopped before a D1 database object was available.

1. Objective

Cloudflare Web Analytics was already enabled. The new counter was not intended to replace analytics; it provides a small public number in the footer, while Web Analytics remains the management/analysis source.

2. Final architecture

Browser
/js/access-counter.js
GET or POST /api/counter
Cloudflare Pages Function
context.env.COUNTER_DB
Cloudflare D1

Published implementation files:

The required D1 binding variable name is COUNTER_DB.

3. Counting semantics

The browser uses sessionStorage key ael-site-visit-counted-v1.

The first load in a tab session sends POST /api/counter, incrementing the global value. Later loads in that same tab session send GET /api/counter without incrementing.

This is therefore not a unique-visitor count. The same person can increment again in a new tab or new session. It is intentionally an approximate public access counter.

D1 stores a single site-wide counter. The counter implementation itself does not store IP address, user agent, page path, device ID, or a persistent visitor ID in D1.

4. Server implementation

The Pages Function creates the D1 table on first successful access if necessary.

site_counter
  counter_key   TEXT PRIMARY KEY
  counter_value INTEGER
  updated_at    TEXT

GET /api/counter returns the current value. POST /api/counter increments with an UPSERT and returns the updated value.

If the D1 binding or operation fails, the public endpoint deliberately returns only HTTP 503 and:

{"ok":false,"error":"counter_unavailable"}

The actual exception is written to Function logs with console.error.

5. Initial production failure

The counter implementation was uploaded with the revised SwitchBot article in commit:

84744e803aa03f4345f14426320fb7799e8044ac
Update SwitchBot article and add public site counter

The Pages deployment itself succeeded, but direct access to /api/counter returned HTTP 503. The footer therefore remained Site visits: — / アクセス数: —.

This demonstrated that a successful build/deploy does not prove that runtime dependencies are correctly attached.

6. Diagnostic process

Initial hypotheses included:

  1. deployment ordering after adding the binding;
  2. failure only in the POST/UPSERT path;
  3. a D1 SQL or table-creation error;
  4. ambiguity between Dashboard configuration and the repository-level wrangler.toml.

Direct browser access performs GET, and GET also returned 503. That ruled out a POST-only failure.

The decisive Pages Functions runtime log was:

counter GET failed
Error: COUNTER_DB D1 binding is not configured

The Function checks context.env.COUNTER_DB before issuing any SQL. The log therefore established that execution stopped before D1 SQL.

7. Removing configuration ambiguity

A repository-root wrangler.toml remained from an earlier optional direct-deployment reference. To keep the Git-connected Pages project Dashboard-managed, it was removed in commit:

b3d3322a9496434b715a0f90c72819805b3822ae
Remove Wrangler config and use Cloudflare Pages dashboard settings

The next build log confirmed:

No Wrangler configuration file found. Continuing.
Found Functions directory at /functions. Uploading.
Compiled Worker successfully

This removed the configuration ambiguity, but this record does not claim that deleting wrangler.toml caused the D1 binding to disappear.

8. Verified root cause and recovery

A later inspection of the Cloudflare Pages settings found that the D1 binding was absent.

The binding was recreated with:

Variable name: COUNTER_DB
D1 database: the public-counter D1 database

The Pages project was redeployed after the binding was restored. The human then confirmed that the public counter counted normally.

The directly verified root cause was therefore:

context.env.COUNTER_DB was absent in the running Pages Function.

Why the binding disappeared from the Pages project settings is not established by this record.

9. Diagnostic sequence that worked

  1. Test /api/counter directly instead of starting from the footer UI.
  2. Record HTTP status and public JSON.
  3. Inspect Pages Functions runtime logs.
  4. Separate Function routing, runtime binding availability, and D1 SQL into distinct layers.
  5. Check the binding in Pages settings.
  6. Redeploy after configuration changes.
  7. Verify the API first, then verify footer rendering.

10. Reusable lessons

Build success is not runtime dependency success

A Pages Function can compile and static assets can deploy while the Function still fails at runtime because a binding is absent.

Keep public errors simple and internal logs specific

counter_unavailable avoids exposing infrastructure details to visitors, while runtime logs retain the precise exception needed for diagnosis.

Binding names must match code exactly

This implementation requires COUNTER_DB; the Dashboard variable name must match it exactly.

Avoid ambiguous configuration authorities

If Dashboard and Wrangler configuration coexist, define deliberately which source is authoritative. This repository now removes the root wrangler.toml and manages the Git-connected Pages project through Dashboard settings.

A public counter is not analytics

This counter is session-oriented and approximate. Cloudflare Web Analytics remains the appropriate source for actual traffic analysis.

11. Privacy boundary

The Cloudflare runtime log supplied during troubleshooting contained client IP, coarse location, browser/user-agent, TLS, and other request metadata. The raw log is therefore not reproduced in this public record; only the diagnostic error text is retained.

The D1 counter schema itself contains no per-visitor record or persistent identifier.

12. Unresolved observations

This record does not establish why the D1 binding disappeared from Pages settings.

A deployment log also showed Hugo v0.147.7 where a different version had previously been intended in Dashboard configuration. That discrepancy was not investigated because it was not required for counter recovery.

13. Final state

As of 2026-08-14:

The structured record is stored under experiments/CLOUDFLARE-COUNTER-001/.