Locking Down Digital Downloads: Atomic Transactions & Range Requests
You sell PDF reports, video tutorials, or software licenses. A customer buys access for 30 days with 5 downloads allowed. A week later, they close their browser during a download—limit exhausted. Or two employees click "Download" simultaneously—both pass the check and the file goes out over the limit. Without a reliable download restriction mechanism, you lose up to 30% of revenue from uncontrolled distribution. We are a team of engineers with 6 years of experience, having implemented over 50 solutions for restricting access to digital goods. Our approach is 10x more reliable than standard CMS modules, which often ignore race conditions and fail to distinguish the first download from a resume. At the core: atomic transactions with row locking SELECT ... FOR UPDATE and HTTP Range request handling. This guarantees each customer downloads exactly as many times as their tariff permits.
Why atomic transactions are critical for limits?
Without locking, two simultaneous requests can both pass the check before the counter updates. The solution is pessimistic row locking. Inside a transaction, we execute SELECT ... FOR UPDATE, re-check the limit, and increment. This ensures no request exceeds the limit even under peak load of 1000 RPS. We implement this pattern in Laravel via repository and Action classes. Additionally, we audit every download in the download_events table.
Types of limits
| Type | Description | Example |
|---|---|---|
| By count | N downloads per purchase | 3 downloads |
| By time | Access until a certain date | 30 days after payment |
| Combined | Both | 5 downloads or 90 days |
| By IP | Only from registered IP | Corporate licenses |
| By device | Tied to fingerprint | For software |
Example configuration by tariff
| Tariff | Download limit | Validity period |
|---|---|---|
| Basic | 5 | 30 days |
| Standard | 20 | 90 days |
| Premium | unlimited | 1 year |
How to prevent race conditions?
To store data, create the table digital_order_downloads:
Schema::create('digital_order_downloads', function (Blueprint $table) { $table->id(); $table->foreignId('order_item_id')->constrained(); $table->foreignId('digital_product_id')->constrained(); $table->string('token', 64)->unique(); $table->integer('downloads_count')->default(0); $table->integer('downloads_limit')->nullable(); // NULL = unlimited $table->timestamp('expires_at')->nullable(); // NULL = never expires $table->boolean('is_revoked')->default(false); // manual block $table->timestamps(); $table->index(['token', 'is_revoked']); }); The DownloadLimitGuard class checks download availability. Problem: two simultaneous requests can both pass the check before the counter updates. Solution—pessimistic row locking. Inside a transaction, we execute SELECT ... FOR UPDATE, re-check the limit, and increment.
class RecordDownloadAction { public function execute(DigitalOrderDownload $download, Request $request): void { DB::transaction(function () use ($download, $request) { $locked = DigitalOrderDownload::lockForUpdate()->findOrFail($download->id); if ($locked->downloads_limit !== null && $locked->downloads_count >= $locked->downloads_limit) { throw new DownloadLimitExceededException(); } $locked->increment('downloads_count'); DownloadEvent::create([ 'digital_order_download_id' => $locked->id, 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'referer' => $request->header('Referer'), 'downloaded_at' => now(), ]); }); } } Handling Range requests: how not to lose a limit on resume
According to the HTTP specification, Range requests are meant for resuming downloads and should not be counted as a new download (see MDN Web Docs on HTTP Range). If every Range request counted as a download, the limit would be exhausted on a single large file. We count only the first request without Range or with Range: bytes=0-. Here’s an example:
public function download(string $token, Request $request): Response { $download = DigitalOrderDownload::where('token', $token)->firstOrFail(); $guard = app(DownloadLimitGuard::class); $result = $guard->check($download); if (!$result->isAllowed()) { return response()->view('digital.download-denied', ['reason' => $result->reason], 403); } $rangeHeader = $request->header('Range'); $isFirstRequest = !$rangeHeader || $rangeHeader === 'bytes=0-'; if ($isFirstRequest) { app(RecordDownloadAction::class)->execute($download, $request); } return $this->streamFile($download->digitalProduct); } How to configure and manage limits?
Different products have default limits, and the tariff overrides them at purchase. In the admin panel, an administrator can reset the counter, extend the expiration, or revoke access. All operations are audited. Three days before expiration, an email reminder is sent by the command php artisan digital:notify-expiring --days=3, scheduled daily via cron. Integration with billing systems is also possible for automatic limit updates upon payment.
Work process
- Analysis—we study your digital goods, tariffs, and limit requirements. Takes 1-2 days.
- Design—we choose a strategy (count/time/IP), design tables and services.
- Implementation—we write code, cover with unit tests (>80% coverage).
- Integration—we embed into your CMS or framework.
- Testing—load tests simulating 5000 concurrent requests, resume download checks.
- Deployment—deploy on your hosting, set up monitoring.
Timelines and cost
Basic implementation (counter + expiration + atomic increment) — from $500 and 2 working days. Complex scenarios (IP/device restrictions, ERP integration) — from $1500 and 5 working days. Cost is calculated individually after requirements analysis. Get an engineer consultation—we’ll evaluate your project for free.
Common mistakes
- Ignoring Range requests—each resume consumes the limit, users complain.
- Lack of transactions—concurrent requests exceed the limit by 2-3 times.
- Hard-binding to IP—mobile users change networks, access gets blocked.
- Too short token—32 characters are insufficient; use 64+.
- Notifications only in English—Russian-speaking users need localization.
Detailed Tariff Limits (click to expand)
| Tariff | Download limit | Validity period |
|---|---|---|
| Basic | 5 | 30 days |
| Standard | 20 | 90 days |
| Premium | unlimited | 1 year |
What’s included
- API documentation (endpoints, request examples).
- Operating instructions for the administrator.
- Access to a private repository with code.
- 3 months of technical support after launch.
- Recommendations for monitoring and scaling.
Contact us to discuss your project. Request an engineer consultation—we will help secure your digital goods.







