Developer Notes

When a Social Post Publishes Without Its Images

A development note about how several reasonable technical decisions can combine into an unexpected problem when a live product is not viewed as one whole system.

A smartphone showing a social media post with a missing image placeholder, surrounded by blurred photo cards in a moody low-light workspace.

Imagine you are creating a post in a social app. You write the text, choose the image that makes the post work, press Share, wait for the upload to finish — and the post goes live without the image.

The most annoying part is that this kind of bug often does not fail loudly. It may affect only a small percentage of users. It may appear once, then disappear, then come back later in a slightly different situation. Even the people who report it may not be able to reproduce it on demand. There is no clean pattern like “old Android devices only” or “this exact iPhone version.” That puts it into the category of floating bugs: problems that are solved more by analysis than by simply reproducing one known failure and patching it.

This case came from a real Expo mobile app: a live social product, already published on iOS and Android, with real users and a long history behind it. That makes the work interesting, but it also changes the responsibility. In an old live system, a small change can touch more than it seems. A function that looks related only to image uploads may also be used somewhere else. A decision that looks strange at first may have been made for a reason. And sometimes, of course, it may not have been made for a good reason at all. The only way to know is to check, not guess.

In a live product, the question is not only “can this be fixed?” It is also “what can this change affect?”

What kind of system is this?

The first step was not the image upload itself. The first step was understanding the system around it: the Expo version, the packages involved, the current app structure, the build situation, and whether the project was in a state where small changes could be made safely.

With Expo, this part matters more than it may look from the outside. Sometimes a small bug sits inside a project that first needs a technical update: Expo version, native dependencies, packages, build configuration, or store-related requirements. In that case, the honest recommendation may be different. It may be better for the client to wait until the next planned technical update instead of spending time and money on a small isolated fix that would require touching too much old infrastructure.

That is part of the developer’s responsibility. The client pays for a specialist not only to write code, but also to understand the real scope, risks, and cost of the work. Sometimes the right answer is “yes, this can be fixed now.” Sometimes it is “yes, but the real cost is bigger than the bug.” And sometimes it is “this is safe to check and patch without turning it into a full app update.”

In this case, the project was in a good enough state for investigation. The Expo setup and related packages did not immediately turn the issue into a larger technical upgrade. That meant it was reasonable to look deeper into the actual publishing flow.

What happens after the user presses Share?

After the project-level check, the next step was to follow the publishing route. What does the screen collect? What is required before publishing? Which functions run after the button is pressed? What does the app send first? What comes back from the server? At what exact moment does the app decide that the post has been published successfully?

That is where the first important detail appeared. Creating a post with images was not one backend action. It was split into two separate steps. First, the app created the post and received a post ID from the server. Then it used that ID to upload and attach the selected images.

At first, this may look like unnecessary complexity. Why not create the post and upload the images together? But the split can make sense. A social post may contain required information that the user spent time entering. If the image upload fails, saving the main post data can prevent the user from losing everything and starting again. I cannot know the original team’s exact reasoning, but that would be a reasonable reason to design the flow this way.

The weak point is the dependency between those two steps. The image upload is not independent. It only works after the post exists and the app has a valid post ID to attach the images to. If the first request succeeds on the server, but the app does not safely receive, store, or use that ID before moving on, the result can become a half-success: the post exists, but the images never get attached.

That does not mean the server literally created a post with null. It can be simpler and more annoying than that. The post creation request may succeed, but the response handling on the app side may fail, timeout, return an unexpected shape, or move forward before the next step is safely prepared. From the backend’s point of view, the post may exist. From the user’s point of view, the published result is broken.

Are the images heavier than the flow expects?

Once the two-step publishing flow was clear, the next question was whether the image upload itself was too heavy for the current implementation. One detail stood out: the images were being processed at full quality.

Full quality sounds good until you remember what modern phones produce. A photo from a current phone can be much larger than a social post needs, especially if the original file is uploaded without resizing or meaningful compression. A live app usually does not need the original camera file as-is. It needs an image that still looks good on screen, loads reliably, and does not make the upload heavier than necessary.

That matters even more in real conditions. Users are not always sitting at home on stable Wi-Fi. They may publish from mobile internet, a weak public network, a train station, a shopping mall, or underground where the connection jumps between “fine” and “barely alive.” A flow that works perfectly during testing can still fail for real users if the files are too heavy and the upload path is too strict.

Is the timeout realistic?

The next important detail was the timeout. The upload request had a fixed 15-second limit, whether the user uploaded one image or several images.

That made the intermittent pattern more understandable. One image on a good connection may pass. Several large images on a weaker connection may not. A slightly slower server response may be enough to push the upload over the limit. The app does not need to be completely broken for the user to get a broken result.

A timeout is not automatically bad. It protects the app from hanging forever. But for image uploads, especially multiple image uploads, a fixed timeout needs to be chosen carefully. A better approach would be to make the upload flow more tolerant: take into account the number of images, the approximate upload conditions, and add a reasonable buffer instead of treating one small image and several large images as the same situation.

What should be changed?

The first change is to make the publishing flow stricter. The app should not start uploading images until the post has definitely been created and a valid post ID exists. If that ID is missing or invalid, the app should stop and handle the problem clearly instead of pretending the flow can continue.

The second change is image optimization. The app should resize images before upload, set a reasonable maximum width and height, and reduce quality to something like 85%. That is usually enough to keep the image looking good inside the app while making the file much lighter and more reliable to upload.

The third change is the timeout strategy. Instead of one fixed 15-second limit for every case, the upload should have a more realistic limit based on what is being uploaded. Several images need more room than one image, and real mobile networks need a little patience. The point is not to wait forever, but to avoid cutting off a normal upload too early.

The fourth change is the user experience around partial failure. If the post is created but the images fail, the app should not behave as if everything is fine. For a developer, that may look like “the main request succeeded.” For a user, it means “my post was published wrong.”

What does this kind of work really require?

The interesting part of this issue is that none of the separate decisions looked completely absurd. Creating the post first makes sense. Uploading images after that makes sense. Having a timeout makes sense. Keeping image quality high sounds good. Moving the user forward after publishing feels natural.

But together, these decisions created a weak publishing flow where the app could succeed just enough to look finished, while still failing at the part the user actually cared about.

That is why debugging a live product is not only about standing above the code and listing what it does. It is about understanding the whole system first: the framework, the dependencies, the risk of touching old code, the user flow, the API sequence, the real network conditions, and the business cost of each possible fix. The best solution is not always the most technically ambitious one. It is the one that solves the user’s problem without creating a bigger one for the product.

— Serhii N.