Pre-emptive uploading of photos to speed up server-side analysis
Figure 1When the photos leave the phone, before and now
1, 2the two photos✓you check the mealAIthe model reads it
On this page
Whenever you upload a meal to Snapkin, we do a lot of processing on the server. That includes AI calls, preparing images to be the right format and size, extracting barcodes or QR codes, you name it. All of that takes CPU time. And that time is very difficult to speed up, because it is computationally heavy or it depends on outside services, like the AI calls.
So what if we could get the images faster?
Two ways to add photos
Snapkin has two ways to add photos to a meal:
- One photo. You can add a meal with just one photo. You don't need to add more, but you can. Take a picture, and add a small description if you feel like sharing some more details.
- Multiple photos, either one by one with our camera, or all at once in the multi-photo mode.
Either way, there is a moment where you have taken your picture, and you are still thinking about an extra description, or you are waiting to take your second photo. In the latest update, we make good use of that time. Every time you take a photo, we already start uploading it behind the scenes. That saves a few valuable seconds that you hopefully notice when using the app: the analysis comes back to you faster.
Why the photos have to arrive first
Some of the work on a meal happens one photo at a time. Reading barcodes and QR codes is a good example. Our server looks for codes in every photo of a meal. If the first look finds nothing, it turns the photo 15 degrees and looks again. Surprisingly, that simple trick finds more barcodes: 10 more out of 196 in our tests. It takes between 0.2 and 0.8 seconds per photo. A hard one takes up to 1.8 seconds. A meal can have six photos.
The photo resolution also got bigger. The phone shrinks every photo before sending it. That used to be 1024 pixels on the long side, about 98 KB. Then we found that 10 of the 79 product codes in real meal photos could only be read after the photo was enlarged. So the phone now sends 2048 pixels, about 330 KB. The AI model still looks at the photo at 1024 pixels, because that is enough.
All of it used to leave the phone in one request, after you tapped Analyse, with the final POST of the meal. On a good connection you mostly did not notice. On a slow one, you waited for an upload that could have started a minute earlier. And the server could not start any of its work yet. It had no idea a photo was coming.
This is what that request looked like for a meal with two photos. Both photos travel inside it, so nothing reaches the server before the last byte of the last photo does.
POST /api/meals
Content-Type: multipart/form-data
photos[] meal-1.jpg (330 KB)
photos[] meal-2.jpg (330 KB)
client_uuid c2a8f0d4-6e1b-4f7a-9d35-0b8e2f61a7c9
note Half the rice
meal_type lunch
eaten_at 2026-10-14T11:42:09.000Z
local_date 2026-10-14
time_zone Europe/Brussels
Upload at the shutter
Behind the scenes, every time you take a photo, a UUID is generated. The phone sends the photo to /api/photo-uploads/{uuid} right away, while the camera is still open. A photo you pick from your library starts the same way, as soon as you add it to a meal. When it reaches our servers, we store it on your user account. We don't attach it to a meal yet.
The same two photos, now sent the moment you take them. One request per photo, each named by its own UUID:
POST /api/photo-uploads/5b597b7e-3b7d-40f3-aad5-a46fc60e1666
Content-Type: multipart/form-data
photo photo.jpg (330 KB)
HTTP/1.1 201 Created
{"uuid": "5b597b7e-3b7d-40f3-aad5-a46fc60e1666", "claimed": false}
POST /api/photo-uploads/4911494d-3d1e-45c0-a9e7-8545287a15b7
Content-Type: multipart/form-data
photo photo.jpg (330 KB)
HTTP/1.1 201 Created
{"uuid": "4911494d-3d1e-45c0-a9e7-8545287a15b7", "claimed": false}
Saving a meal is the very last step we do in the app. It is when you tap Analyse, and it is when we receive all of the information about your meal, including the description and the extra photos. The meal no longer carries the photos themselves. They are already uploaded. It carries their UUIDs.
So when you tap Analyse, the meal is a few hundred bytes of JSON. The photos[] files are gone, and uploads names the two photos the server already has, in the meal's order:
POST /api/meals
Content-Type: application/json
{
"client_uuid": "c2a8f0d4-6e1b-4f7a-9d35-0b8e2f61a7c9",
"uploads": [
"5b597b7e-3b7d-40f3-aad5-a46fc60e1666",
"4911494d-3d1e-45c0-a9e7-8545287a15b7"
],
"photo_codes": {},
"note": "Half the rice",
"meal_type": "lunch",
"eaten_at": "2026-10-14T11:42:09.000Z",
"local_date": "2026-10-14",
"time_zone": "Europe/Brussels"
}
HTTP/1.1 202 Accepted
The phone sends one photo at a time, oldest first. If you tap Analyse while a photo is still on its way, the meal waits for that upload. It does not start a second copy.
From the outside, nothing changed. You take a photo, you tap Analyse, a number appears. The work behind the scenes moved. Your screen did not.
Figure 2The same three taps, in the real app
- You take a photoThe photo gets an id. Its upload starts in the background.
- You check the mealAdd a note, take another photo. Each new photo joins the queue behind the first.
- You tap AnalyseThe meal sends only the ids. The photos are already there.
A photo with no meal yet
So the server now holds photos that belong to no meal yet. You might take three photos and keep one. You might close the camera and walk away. A photo waits until a meal claims it.
Every time you delete one of the photos, or you never end up sending the meal, we delete that data on our side. A deleted or retaken photo is gone right away. A photo that no meal ever claims is deleted after 72 hours. Privacy, as always, remains a very high priority for me. This strikes a balance between the best user experience, the fastest analysis we can make, and not storing information we don't actually need.
Three things can happen to a waiting photo:
- a meal claims it,
- you delete it,
- or the 72 hours run out.
Two of them can happen at the same moment. So each one locks the photo's row first. A photo ends up in a meal or gets deleted, never both. If you deleted a photo and its upload arrives later, the server answers 410 Gone. It keeps none of the photo.
When the server does not have it
Sending early makes things faster. It must never lose a meal. So the server checks. If the meal names a photo the server never received, the server answers 422 with the ids it is missing. The phone sends those photos again and retries the meal.
If photos are still missing after that, the phone sends the meal the old way: the whole meal, photos inside, in one request. It does the same when a photo was deleted, or when the server has no upload address at all. Older versions of the app only know the old way. So the server still accepts it.
Figure 3When the server is missing a photo
The meal names two photos, A and B. Photo B never reached the server.
POST /api/mealsids A and BThe meal names both photos.
422missing: BThe server has no Photo B, and says so.
POST /api/photo-uploads/BPhoto BOnly the missing photo is sent again.
201storedPhoto B is on the server now.
POST /api/mealsids A and BThe same meal, sent again.
202acceptedBoth photos are there. The analysis starts.
410 Gone is different: a photo deleted on the phone stays deleted, even if its upload arrives later.
The upload that arrived in half
One problem we did not expect was in PHP itself. When a connection drops in the middle of an upload, PHP marks the file it was receiving as incomplete. Everything after that point is lost. But PHP still runs the request. So the server stored the photo, without the extra data the phone sent after it. On the next try, the server told the phone the photo was already there. It was, but not all of it.
The server now refuses an upload that was cut off, with 400 upload_incomplete. The phone's next try is then stored in full. The fix is one check, shared by the photo upload and the meal.
A retry is only safe if the server can tell a finished upload from a broken one.
What it saves
We upload those images, resize them, extract whatever QR code or barcode we can, and set everything up for the AI call that has to follow next. That call no longer waits for the photos. It is all set up.
At the moment, this is probably saving 1 to 2 seconds of waiting for you. That time is saved while uploading and processing on our side. The biggest delay now still remains the AI calls, but there isn't much blocking anymore until we get to that point.
There is also a cost. A photo you retake has already been uploaded. We delete it right away, and it is never analysed. But on a slow mobile plan, it still used some of your data.