Urgency and scarcity elements with countdown timer on website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Urgency and scarcity elements with countdown timer on website
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing Urgency/Scarcity Elements (Timer, Limited Quantity) on Website

Urgency (time pressure) and scarcity (scarcity) — classic Cialdini principles, working in e-commerce for 20 years. Problem: most implementations are either technically unreliable (timer resets on page refresh) or look obviously manipulative (counter "3 left" that never changes). Below is implementation that works honestly and technically correctly.

Countdown Timer

Reliability requirement: timer should not reset on page refresh. Cannot do this via new Date() + N minutes on each component mount.

Correct scheme:

  1. On first visit to promotion, create Redis record with TTL
  2. On subsequent visits, get remaining time from Redis
  3. For unauthenticated — key by sessionId from cookie
// Get or create session timer
public function getCountdown(Request $request, string $promoCode): array
{
    $sessionId = $request->cookie('session_id') ?? Str::uuid()->toString();
    $key = "countdown:{$promoCode}:{$sessionId}";

    $ttl = Redis::ttl($key);

    if ($ttl <= 0) {
        $duration = 1800; // 30 minutes
        Redis::setex($key, $duration, now()->addSeconds($duration)->timestamp);
        $ttl = $duration;
    }

    return [
        'ends_at'     => now()->addSeconds($ttl)->toIso8601String(),
        'session_id'  => $sessionId,
    ];
}

Timer component (React):

const CountdownTimer: React.FC<{ endsAt: string }> = ({ endsAt }) => {
  const [timeLeft, setTimeLeft] = useState(0);

  useEffect(() => {
    const target = new Date(endsAt).getTime();

    const tick = () => {
      const diff = Math.max(0, target - Date.now());
      setTimeLeft(diff);
    };

    tick();
    const interval = setInterval(tick, 1000);
    return () => clearInterval(interval);
  }, [endsAt]);

  const hours   = Math.floor(timeLeft / 3_600_000);
  const minutes = Math.floor((timeLeft % 3_600_000) / 60_000);
  const seconds = Math.floor((timeLeft % 60_000) / 1000);

  if (timeLeft === 0) return <ExpiredBanner />;

  return (
    <div className="countdown" role="timer" aria-live="polite">
      <Digit value={hours}   label="h" />
      <Digit value={minutes} label="m" />
      <Digit value={seconds} label="s" />
    </div>
  );
};

Component Digit adds flip animation on value change via CSS @keyframes.

Stock Indicator

Show real stock from inventory system — best practice. If quantity ≤ N, show warning:

const StockIndicator: React.FC<{ stock: number }> = ({ stock }) => {
  if (stock > 10) return null;

  return (
    <div className={`stock-indicator stock-indicator--${stock <= 3 ? 'critical' : 'low'}`}>
      {stock <= 3
        ? `Only ${stock} left`
        : `${stock} left — running out`}
    </div>
  );
};

Sync with real warehouse — if using 1C or WMS, pull via API and cache in Redis for 5 minutes:

public function getStockLevel(int $productId): int
{
    return Cache::remember("stock:{$productId}", 300, function () use ($productId) {
        return $this->warehouseApi->getAvailableQuantity($productId);
    });
}

Important: don't show exact stock for very popular items — this creates herd behavior effect and slightly strengthens conversion.

"X People Viewing Right Now"

Counter of active users on product page — real or approximate.

Real implementation via Redis:

// On product page load
public function trackView(int $productId, string $sessionId): int
{
    $key = "viewers:{$productId}";
    Redis::zadd($key, time(), $sessionId);
    Redis::zremrangebyscore($key, 0, time() - 300); // remove older than 5 min
    Redis::expire($key, 600);

    return Redis::zcard($key);
}

For real-time update — either polling every 30 seconds or Server-Sent Events:

// SSE endpoint
public function viewersStream(int $productId): StreamedResponse
{
    return response()->stream(function () use ($productId) {
        while (true) {
            $count = $this->viewerService->getCount($productId);
            echo "data: {\"viewers\": {$count}}\n\n";
            ob_flush();
            flush();
            sleep(30);
        }
    }, 200, [
        'Content-Type'  => 'text/event-stream',
        'Cache-Control' => 'no-cache',
    ]);
}
// On client
useEffect(() => {
  const es = new EventSource(`/api/products/${productId}/viewers`);
  es.onmessage = (e) => setViewers(JSON.parse(e.data).viewers);
  return () => es.close();
}, [productId]);

Flash Sale

Flash sale requires atomic work with stock — no race conditions:

// Lua script in Redis for atomic decrement
$script = <<<'LUA'
local key = KEYS[1]
local current = tonumber(redis.call('GET', key) or '0')
if current <= 0 then
    return -1
end
return redis.call('DECR', key)
LUA;

$remaining = Redis::eval($script, 1, "flash_sale:{$saleId}:stock");

if ($remaining < 0) {
    return response()->json(['error' => 'Product out of stock'], 422);
}

"Last in Cart"

If product is in other users' carts and real stock matches or is below reserved quantity:

{isLastInCart && (
  <Alert variant="warning">
    This product is in other buyers' carts. Checkout to reserve it.
  </Alert>
)}

Technical implementation: cart_reservations(product_id, quantity, session_id, expires_at) table. Reservation lifted after 30 minutes or on order completion.

Ethics and Anti-Patterns

Urgency elements work when honest. Patterns that harm reputation:

  • Timer that resets on every page visit
  • "2 left" on item with constant stock
  • Viewer counter randomly generated on client

These tricks boost conversion short-term but destroy trust long-term — users notice inconsistencies.

Timeline

Task Time
Countdown timer (Redis + component) 1 day
Stock indicator (real data) 0.5 day
Viewer counter (SSE) 1 day
Flash sale with Redis reservation 1–2 days

Basic set (timer + stock): 1.5–2 days.