The editor uploads one image into five records — the budget is eaten by duplicates, disk grows, pages slow down.
We develop media libraries for CMS to avoid these issues. A centralized repository with smart search and automatic optimization solves the duplicate problem. In one project, a client lost 40% of disk space due to repeated files. After implementing our library, space was freed, and page loading doubled in speed. We use cloud storage on S3. Asynchronous preview generation and presigned URLs reduce load.
How to speed up image upload?
Presigned URL is a technique where the file goes directly from the browser to S3, bypassing the server. The server only generates a temporary link and confirms the upload. This reduces PHP load to zero: even 4K videos upload in seconds instead of hanging on max_execution_time. Server load drops by 5-10 times, saving up to 150,000 rubles per year on infrastructure. This approach eliminates the risk of server overload.
| Upload Method | Server Load | Access Time | Security |
|---|---|---|---|
| Direct to server | High (PHP waits) | 300-600 ms | File may not be checked |
| Presigned URL | Minimal | 50-100 ms | Valid for 15 minutes |
The comparison shows: presigned URL is 5-10 times faster and more secure. We use this method in all projects — it is tested under load of up to 500 simultaneous uploads.
Why should preview generation be asynchronous?
After image upload, we launch a queue (based on Redis) to create previews. The user doesn't wait — in a few seconds all versions are ready.
class GenerateImageConversions implements ShouldQueue { public function handle(): void { $conversions = [ 'thumb' => ['width' => 150, 'height' => 150], 'medium' => ['width' => 800, 'height' => null], 'large' => ['width' => 1920, 'height' => null], 'webp' => ['width' => null, 'height' => null, 'format' => 'webp'] ]; foreach ($conversions as $name => $params) { $image = Image::make(Storage::get($this->media->path)); if ($params['width']) { $image->resize($params['width'], $params['height'], fn($c) => $c->aspectRatio()); } if (isset($params['format'])) { $image->encode($params['format'], 85); } Storage::put("media/conversions/{$this->media->id}/{$name}", $image->stream()); } } } This approach saves 40% of disk space by converting to WebP and resizing to required dimensions. Conversion reduces traffic by 30–50%, saving up to 50,000 rubles on bandwidth. Contact us — we will select optimal queue settings for your infrastructure.
How to organize search and filtering?
Search in the media library is built on indexing fields: file name, alt text, title, and caption. For standard volumes (up to 50,000 files), a full-text MySQL index is sufficient. If there are more files, we connect Elasticsearch — it provides search in milliseconds even with millions of records. Filters by file type, date, and size speed up navigation.
| Search Method | Speed | Implementation Complexity | Suitable For |
|---|---|---|---|
| Full-text index | Fast (up to 50k) | Low | Small to medium projects |
| Elasticsearch | Instant (1M+) | Medium | Large projects with search |
We will help you choose the right option during analysis.
Media library interface
Main UI capabilities:
- File grid with previews (or list for performance with 10k+ files)
- Filters: file type, folder, date, size
- Search by name, alt text, and custom fields
- Drag-and-drop upload with progress indication
- Folders and subfolders for structuring
- Multi-select and bulk operations (delete, move, edit alt)
- Edit alt, title, caption directly in the library
- Copy URL to clipboard with one click
We build the editor picker widget on React (or vanilla JS for older CMS):
function MediaPickerButton({ onSelect, multiple = false }) { const [isOpen, setIsOpen] = useState(false); return ( <> <Button onClick={() => setIsOpen(true)}>Choose from library</Button> <Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogContent className="max-w-4xl h-[80vh]"> <MediaLibraryGrid onSelect={(files) => { onSelect(multiple ? files : files[0]); setIsOpen(false); }} multiple={multiple} /> </DialogContent> </Dialog> </> ); } Data model
Data model (click to expand)
media_files ( id, original_name, file_name, disk, path, mime_type, size, width, height, -- for images alt, title, caption, -- SEO and accessibility folder_id, uploaded_by, created_at, updated_at ) media_folders ( id, name, parent_id, created_at ) media_conversions ( id, media_id, name, -- 'thumb', 'medium', 'large' file_name, width, height, size, created_at ) What is included in the work
- REST API for media file management (Swagger documentation)
- Server-side presigned URL generation
- Asynchronous queue processing (up to 50 files per minute on one worker)
- Editor picker widget (React/vanilla) with multi-select support
- Scheduled job configuration for unused file cleanup
- Editor team training (1 hour)
- 3-month warranty on identified bugs
Work process
- Analysis — study current load, file types, integrations
- Prototyping — design interface, agree on data model
- Development — implement backend (Laravel 11 + S3) and frontend (React 18)
- Testing — load testing up to 500 simultaneous uploads, memory leak checks
- Deployment — queue setup, CI/CD, monitoring via Grafana
Timeline and cost
A typical project takes 3 to 4 weeks. Cost is calculated individually — depends on integration complexity and required customizations. Order a consultation to evaluate your project.
Typical mistakes when creating a media library
- Generating previews synchronously — user waits, server freezes
- Storing on local disk — grows to hundreds of gigabytes
- No cleanup — junk takes up to 30% of space
- Tight coupling to one CMS — difficult to reuse
We have encountered all these pitfalls in our own projects and guarantee that your media library will be fast, scalable, and user-friendly. Order a turnkey media library development — get a free consultation.







