Synchronous scanning holds the user's request open while an uploaded file is checked, returning the verdict with the response. Asynchronous scanning accepts the file immediately and checks it in the background, updating its status afterwards. The choice decides where latency, complexity and risk sit in the upload.
The two shapes
Synchronous is the shape most people build first, and it is a single line of reasoning. The file arrives, your code sends it to be checked, waits, and responds according to the answer. Accepted files are stored, rejected files never were.
Its great virtue is that there is no gap. At no point does an unchecked file exist anywhere your application would serve it from, because nothing is stored until the verdict comes back. That property is free rather than engineered, and free properties are worth a lot.
Asynchronous separates acceptance from verification. The file is taken, put somewhere holding, and the user gets an immediate response saying it arrived. Scanning happens on a worker or in response to a storage event, and when the verdict lands the file's status changes: released for use, or rejected with the user informed.
This is how the major cloud providers structure malware scanning for object storage: AWS triggers a scan on upload to a bucket, and Google Cloud publishes the same pattern as a reference architecture. A file lands in a bucket, the write triggers a scan, and the result moves the object or tags it. If your uploads already go to storage before your application sees them, this is the grain of the system rather than a choice you are imposing on it.
What you are actually trading
| Synchronous | Asynchronous | |
|---|---|---|
| User waits | Yes, for the whole check | No, only for the upload |
| Unverified file exists | Never stored | Yes, until the verdict lands |
| Complexity | One path, one verdict | States, queues, notifications, retries |
| Large files | Poor, connections held open | Good, decoupled from the request |
| Telling the user it failed | In the response | Needs a channel that may not exist |
The row that catches teams out is the last one. In a synchronous design, rejecting a file is trivial, because the user is still there. In an asynchronous design they may have closed the tab, and you now need somewhere to put the news: a notification, an email, a status that is visible next time they look. Applications frequently build the asynchronous pipeline and discover the feedback problem afterwards.
The second row is the one that matters for security, and it is the reason an asynchronous design has to be built carefully rather than merely built.
The rule that makes asynchronous safe
There is really only one, and everything else follows from it: an unverified file must not be reachable.
Concretely, that means the file lands in a quarantine location that nothing user-facing reads from. Not your public bucket with a status column set to pending, because something will eventually read that bucket without checking the column. A separate location, with the move to the served location being the thing that marks it clean.
Two related habits reinforce it. Do not return a usable URL at upload time, since a URL that works immediately is precisely the window an attacker wants. And make the released state the only one your serving code recognises, so that a file which never completed scanning is invisible by default rather than by an explicit check somebody might omit.
That last point is the difference between a design that is safe and one that is currently safe. If serving requires proof of clearance, a bug in the scanning pipeline fails towards nothing being served. If serving merely avoids files marked bad, the same bug fails towards everything being served.
You also need to decide what happens when the scan never completes at all, which is the fail-open or fail-closed question. An asynchronous design makes the better answer available, because a file can simply stay in quarantine until the backlog clears, rather than being accepted or refused in the moment.
Choosing, and choosing both
Four questions get you most of the way:
- How big are the files, honestly? Not the limit in your configuration, the sizes users actually send. If almost everything is a few megabytes, synchronous is comfortable
- What does the user do next? If the following screen depends on the file being accepted, waiting is better than a pending state that complicates the whole flow
- Where does the file land first? If uploads go directly to object storage, asynchronous already fits the architecture
- Do you have a way to tell them later? If not, that has to be built before an asynchronous rejection is a usable outcome
And the answer for many applications is both. Handle small files synchronously so the common case stays immediate and simple, and route above a size threshold, or when a batch arrives, to the asynchronous path. Two code paths is a genuine cost, and it usually beats making every user wait for the worst case or putting every upload through a pending state it did not need.
Our guide to file upload security covers how this fits with validation, storage and the rest of the pipeline.
Pick the shape that suits the upload, rather than the one your provider supports. The Red Eagle Tech CDR API offers both synchronous and asynchronous endpoints, so small files can be handled inline and larger ones queued, without running two suppliers. Published per-document pricing, OAuth2, and you can be running in minutes.