A New Architecture for the Virus and Malware Scan API: Faster Scans, Same Endpoints
We have rebuilt the infrastructure behind the attachmentAV Virus and Malware Scan API (SaaS). All five regions (Europe, the United States, Canada, India, and Australia) now run on the new architecture. Your endpoints, API keys, and SDKs stay the same, so you don’t have to change anything. What you will notice is the speed: a synchronous scan of a 5 MB file takes 366 ms instead of 479 ms, which is 24% faster.

What changed behind the scenes
Until now, every request passed through an API gateway before it reached the scan fleet, the servers that run the Sophos engine. The gateway checked your API key, applied the throttling and quota of your plan, and forwarded the request. An asynchronous scan job took one more detour through a serverless function before it landed in the job queue.
The new architecture removes these hops:
- Your request goes from the load balancer straight to the scan fleet.
- The scan fleet checks your API key and enforces throttling and quotas itself. It reads your subscription from an in-memory cache in the same region.
- The scan fleet also accepts asynchronous scan jobs, stores the scan results you poll for, and records callback failures. Separate components did that before.
Each region is self-contained. A request is authenticated, throttled, and scanned within the region of your subscription, and no region depends on another one to serve requests.
The result: lower latency
Right after migrating the Europe region, we sent the same requests to the old and the new API and compared the total latency.
| Total latency | p50 | p90 | max |
|---|---|---|---|
| Synchronous scan of a 5 MB file, old API | 479 ms | 529 ms | 803 ms |
| Synchronous scan of a 5 MB file, new API | 366 ms | 434 ms | 447 ms |
| Submit an asynchronous scan job, old API | 138 ms | 161 ms | 219 ms |
| Submit an asynchronous scan job, new API | 101 ms | 109 ms | 154 ms |
- Synchronous scans are 24% faster. The median dropped by 113 ms, and the new API was faster in each of the 30 paired measurements. The slowest request took 447 ms on the new API and 803 ms on the old one.
- Submitting a scan job is 27% faster. Setting up the connection takes about 62 ms on both APIs. That leaves 37 ms for the request itself on the new API and 74 ms on the old one.
- No slow first request. The first job we submitted to the old API took 694 ms. The new API had no slow first request.
This is how we measured:
- We sent 33 requests per API and test, alternating between the old and the new API with a pause of 0.5 seconds. The first 3 requests count as warm-up, which leaves 30 measured requests.
- Each request opened a new connection, so the numbers include the TCP and TLS handshake.
- The synchronous scan uploaded 5 MB of random data to
POST /v1/scan/sync/binary, with a new payload for every request. - The asynchronous test submitted a small file by URL to
POST /v1/scan/async/download.
Your numbers depend on where your application runs, on the size and type of your files, and on whether you reuse connections. If you send many requests, keep the connection open: that saves the connection setup on every request after the first.
What stays the same
The migration required no change on your side:
- The API endpoints of all five regions.
- Your API keys and your tenant ID.
- The requests and the successful responses of all actions.
- The signature of callbacks. We sign with the same keys as before.
- The throttling and quota limits of your plan.
- The SDKs. You don’t need to update them.
What gets better
Besides the lower latency, the new architecture comes with a few improvements.
Only scans count against your quota
Until now, every request counted against your monthly quota, even if it did not scan a file. From now on, only scans (POST /v1/scan/...) count. These requests no longer count:
GET /v1/scan/async/resultGET /v1/callback/failuresGET /v1/usageGET /v1/whoamiGET /v1/test
If you poll for the results of asynchronous scans, you get more scans out of your plan. These requests also keep working after you have used up your quota, so you can still fetch the results of the scan jobs you submitted before. Throttling applies to all requests as before.
We did not carry over the usage of the current month to the new infrastructure either. Your monthly quota started fresh on the day we migrated your region.
Retry-After header for throttled requests
When you exceed the throttling limit of your plan, the API responds with the status code 429. This response now includes a Retry-After header with the number of seconds to wait before you send the next request.
The upload limit of /v1/scan/sync/form applies to the file
The 10 MB limit of POST /v1/scan/sync/form used to apply to the whole request body, so the largest file you could upload was 10 MB minus the multipart/form-data overhead. The limit now applies to the file itself.
Accurate status codes for asynchronous scans
The old API answered POST /v1/scan/async/download and POST /v1/scan/async/s3 with the status code 204 in some cases where the scan job was not accepted, for example after an internal error. The new API returns the actual status code, so your application can retry.
Callback failures no longer include the response body
When a callback fails, we no longer store the response body that your callback URL returned. Therefore, the body is not part of the error_json of new failures when you list callback failures.
Changes to error responses
If your application only checks the HTTP status code of failed requests, nothing changes for you. If it reads the response body of errors, please check the following changes.
Error responses contain the property error with a description of the error:
{"error": "Invalid API key."}
Some error responses contain the additional properties code, message, and statusCode:
{"error": "file was empty", "code": "bucketav:empty-file", "message": "file was empty"}
The properties code, message, and statusCode are deprecated, and we will remove them in the future. Use the HTTP status code and the property error instead.
Three more details changed:
- A request to an unknown path returns
404with{"error": "Not Found"}instead of403. - A validation error (
400) reports the first problem of the request instead of all of them. - For a few errors, the property
errornow contains a description of the error instead of the HTTP reason phrase (for example,Payload Too Large).
The API Definition lists the status codes of error responses.
What’s next
The new architecture is also the foundation for what we build next. We are working on a dashboard to manage your subscription: API keys, usage, and quotas in one place.
Do you have questions about the migration, or did you notice anything unexpected? Let us know!
Published on November 13, 2026 | Written by Andreas