[KongchangAI]
· 1 min read· 992 words

Short Link TTL Design Lessons: Expiration Is a Feature, Not a Cron Job

Short Link TTL Design Lessons: Expiration Is a Feature, Not a Cron Job

Short link TTL should not rely on background sweeping but validate expiration at read time in the query.

Using a short link creator, the article shows that background-sweeper TTL fails in three ways: clock skew between database and API servers makes expiration uncertain; the sweeper depends on process liveness, so a restart silently stops expiration and temporary links permanently occupy slugs; delete and lookup are not atomic, so a request can pass the check and then crash when the sweeper deletes the row. The fix is to embed the expiration condition in the query, making the database the single source of truth, with the sweeper relegated to lazy reclamation. The redirect handler returns the same 410 for missing and expired rows. This pattern matters especially for one-time resources passed between AI agents.

When many developers implement the temporary nature (TTL) of short links, their first instinct is to add a background sweeper task: scan the table every minute and delete rows where expires_at < now(). Simple and direct, but the author found while building a short link creator that this approach silently breaks in three places.

Short link TTL design lessons

Clock Skew: The Invisible Race Condition

The database server and API server clocks differ by 400 milliseconds, causing a link with a 60-second TTL to potentially live for 60.4 seconds, or expire at 59.6 seconds—depending on which process touched it last. To a human user this doesn't matter, but for an AI agent that "polls after the task completes," the difference between "still valid" and "410 Gone" is a race condition in someone else's workflow.

Clock Skew refers to differences between local clocks on different machines in a distributed system, typically caused by hardware crystal oscillator drift, NTP synchronization delays, or virtual machine scheduling jitter. Even when all nodes have NTP enabled, second-level or even hundred-millisecond-level deviations remain common. In TTL scenarios, if expiration judgments occur separately in the API process and the database process, with each side comparing expires_at using its local time, then the same row of data may yield opposite survival conclusions at different moments. More insidious is that this deviation is not a fixed value but fluctuates with load and synchronization cycles. The general engineering countermeasure is: time-sensitive judgments should trust only a single time source (usually the database's now()), or use a monotonic clock rather than wall-clock time to measure time intervals.

Sweeper Depends on Process Liveness

The sweeper only runs while the process is alive. Restart the bot, and expiration logic silently stops. Links that should be short-lived become "squatters" permanently occupying the slug namespace. The author found a "temporary" link from six months ago that still resolved to a long-forgotten test webhook payload.

Lazy Reclamation is a resource cleanup strategy that contrasts with background sweeping: instead of proactively and periodically deleting expired data, it judges and cleans up opportunistically on the read or write path. Its core advantage is decoupling "cleanup" from "access"—even if the cleanup process never runs, correctness is unaffected; only storage space is temporarily unreleased. Lazy reclamation is typically used together with "read-time validation": the read path is responsible for semantic correctness (expired means invisible), while the write path or low-frequency background tasks handle physical reclamation. This layering keeps system behavior consistent even during process crashes, deployment interruptions, or scheduler failures, at the cost that expired rows may continue occupying storage and unique constraints (such as the slug namespace) for a period, so reclamation tasks should still exist, but only as an optimization rather than a correctness dependency.

Delete and Lookup Are Not Atomic

A request can pass the expiration check, then be scheduled out, then the sweeper deletes the row, then the request comes back to attempt to serve the redirect and crashes because the row is missing. The fix is not a transaction, but moving the expiration check into the query itself:

WHERE slug = ? AND (expires_at IS NULL OR expires_at > now())

The database becomes the single source of truth for liveness. No sweeper, no skew, no time window.

The Right Approach: Read-Time Validation + Lazy Reclamation

Now TTL is enforced at read time, and the sweeper is only responsible for reclaiming already-expired rows (it becomes a garbage collector, not a gatekeeper). The redirect handler treats "row does not exist" and "row has expired" identically—the same 410, the same response body. The caller cannot distinguish "never existed" from "expired five seconds ago," which is exactly the semantics that short-lived objects need.

The author summarized the lesson in one sentence: If what you build promises to disappear, don't let disappearance be a side effect of process liveness. Make "nonexistence" the default state, and "existence" the temporary exception. Now even if you forget to run the sweeper, short links expire on their own—the author has indeed forgotten several times, and the links still did the right thing.

The tool is packaged at x402.freeq.one/tools/shortlink_create.html, supporting slug, TTL, and an admin key for statistics and deletion.

Implications for AI Agent Workflows

The value of this case extends beyond short links. When AI agents hand off tasks, pass one-time documents, or event invitations, "temporariness" is the core contract. If expiration depends on a background process that may crash, what the agent receives is an unreliable promise. Internalizing expiration checks into every read and using the database's current time for judgment is what makes "automatic disappearance" dependable behavior.

The author did not use distributed locks or complex transaction isolation, but solved the problem with a single WHERE condition—sometimes the most robust design is to let the data layer make the decision for you.

One-line Summary

Short link TTL should not depend on background sweeping, but should validate expiration at read time within the query.

Paragraph Summary

Through the practice of building a short link creator, the article points out that implementing TTL with a background sweeper task fails in three ways: clock skew between the database and API server causes uncertain expiration times; the sweeper depends on process liveness, and after a restart the expiration logic silently stops, turning temporary links into permanent slug occupants; delete and lookup are not atomic, and a request may pass the expiration check and then crash when the row is deleted by the sweeper. The correct solution is to write the expiration condition into the query itself, making the database the single source of truth for liveness, with the sweeper downgraded to a garbage collector for lazy reclamation. The redirect handler returns the same 410 for "row does not exist" and "row has expired," so callers need not distinguish. This pattern is especially important for passing one-time resources between AI agents.

#short link TTL#clock skew#lazy reclamation#read-time validation#AI agent workflow
Share:

Related articles